Skip to content

Chore/v1 p0 blockers - #28

Merged
ayan-de merged 16 commits into
mainfrom
chore/v1-p0-blockers
Sep 5, 2026
Merged

Chore/v1 p0 blockers#28
ayan-de merged 16 commits into
mainfrom
chore/v1-p0-blockers

Conversation

@ayan-de

Copy link
Copy Markdown
Owner

No description provided.

ayan-deand others added 16 commits September 5, 2026 16:49
The README pitched FreeCode as a tool that "drives AI coding assistants via
browser automation" with a two-phase "ask which files, then send them" flow.
That is the legacy Playwright path CLAUDE.md marks as not wired into the
primary path. The product is a ~198-provider API agent loop.
Rewritten against source: the real installer, all 16 tools, the command list,
the four-frontend architecture, and the generic provider driver. The browser
path stays as a blockquote noting it exists and is not the default.
Three claims were corrected while writing: `freecode session` is list + delete
only, `freecode memory` is graph stats/rebuild/UI, and `freecode update` is
injected by the TUI (apps/tui/src/entry.ts:194), not registered in
create-cli.ts.
Carries the subscription-auth warning callout too — it sits inside the
rewritten body, and the docs page it links to lands later in this branch.
beforeStable.md P0 #1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
Three ways a headless run diverged from an interactive one:
- **No hooks.** HookSettingsManager and registerRtkHook were constructed only
in startServer(), so `freecode run` loaded no settings.json hooks — the
formatter that fires after every edit interactively silently did not fire in
CI, same repo, two behaviours. Both now live in hooks/bootstrap.ts
(initHooks), called by startServer() and the run handler. Only `serve` passes
watch: true; a one-shot run exits before a settings edit could apply and
would otherwise hold an fs watcher open past its last turn.
- **`build` mode denied every write.** askPermission rejects immediately with
no frontend listening and promptForPermission maps that to deny, while
build's default for mutating tools is ask. So `freecode run "fix the test"`
read fine and was denied every edit. `--yes` (-y) answers the ask tier with
allow; repeatable `--allow <rule>` adds in-memory session grants. Both ride
in on new AgentLoopConfig.autoApproveAsks / .sessionGrants.
Scoped to the ask tier deliberately: a deny rule and a read-only mode still
refuse, because those are decisions someone already made, not questions
waiting for an answer.
- **`--agent` was an unchecked cast**, so `--agent buld` fell through
modeDefault's default branch and ran with build semantics. Added yargs
.choices(), as `mcp add`'s type already has.
Tests: agent/headless-permission.test.ts (5 — the first pins that the
unattended default is still deny, two land a real write on disk via --yes and
--allow, two assert --yes beats neither a deny rule nor plan mode) and
hooks/bootstrap.test.ts (2).
beforeStable.md P0 #2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
`config.get` returned readConfig() verbatim, API keys and web-session cookies
included. The full JSON-RPC surface is reachable over web-server.ts's POST
/api — token-gated and loopback by default, but `host` is a parameter, so one
`--host 0.0.0.0` turned a debug convenience into key exfiltration.
redactConfig() builds the safe view field by field: { hasApiKey, model?,
authMode? } per provider entry, { hasCredential } per web entry, with current,
lastAgentMode and recovery passed through. An allowlist rather than a blocklist
of known secret names — WebCredentials has grown a secret-bearing field twice,
and a blocklist is wrong the day it grows a third.
No consumer lost anything. The only reader of the raw shape was the TUI's
getConfig() wrapper, which has no call sites; its ConfigInfo type now matches.
Every "is this provider set up" question already went through providers.list's
hasApiKey, which never carried the key.
Tests: providers/config-redaction.test.ts (4 — no secret from any block
survives serialization, the exact kept shape, an anonymous web session reads as
hasCredential: false rather than a missing block, an empty config stays empty).
beforeStable.md P0 #3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
It removed all of ~/.freecode on one `y` — sessions, rollout logs, memory,
prompt history, usage — and the prompt did not say so.
scripts/uninstall.sh already drew the right line (--purge for a full wipe, data
kept otherwise); the CLI command was the copy that got it wrong, so the fix is
to make them agree rather than invent a third behaviour.
- Default takes the launcher and ~/.freecode/builds — the program. Everything
else stays.
- `--purge` takes the directory, and the line it prints names what is inside it
rather than the path alone.
- `--dry-run` added; `--force` gained -y/--yes so both entry points accept the
same flags.
No `--keep-data`: keeping data is now the default, and a flag for the default
reads like the other behaviour is still lurking.
Two things fell out. A non-TTY stdin resolved readline.question at once with an
empty answer, which the old code read as "no" — correct by accident, and silent;
it is now an error naming --force. And the isDirectory() branch before
rmSync/unlinkSync is gone, since rmSync with recursive covers both.
Tests: cli/commands/uninstall.test.ts (5, against a pure planUninstall() so the
safety rule is asserted without deleting anything).
The separate gap that uninstall ignores FREECODE_HOME / FREECODE_INSTALL_DIR
and misses the Windows launcher path is untouched and still tracked; it takes
the fixed item's place in the CLI page's known gaps.
beforeStable.md P0 #4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
§0.1 of the OAuth spec was the risk section and had no resolution. A v1 that
ships subscription auth as a headline feature needs an explicit, user-visible
stance — not a spec paragraph.
Resolved as: ship it, state the risk plainly, do not euphemize. Recorded in the
spec as §0.2 and, more importantly, published:
- /getting-started/anthropic-subscription — a new page whose second section is
the risk, before any instructions. FreeCode presents itself to Anthropic as
Claude Code; Anthropic reserves subscription inference for its own surfaces
and has acted against tools doing this; the account at risk is the user's.
Then the three opt-ins and the narrow fourth (no key configured plus a
freecode login — an imported Claude Code login deliberately does not count),
the login flow, status/logout, the org-forbidden 403 latch and API-key
fallback, and why cost is stamped on the call.
- /reference/cli gained a `freecode auth` section; the command was undocumented
entirely.
- /reference/env gained FREECODE_ANTHROPIC_AUTH, flagged as an opt-in rather
than a neutral switch.
The code needed nothing: auth.ts already printed the §0.1 disclosure on login
and the identity block was already quarantined to the OAuth path with a test.
The gap was that a user could only read any of it after deciding to run the
command.
Not adopted: a y/N confirmation on login. §0.1 chose "print once, no repeated
nagging" deliberately, and a prompt in front of a command the user just typed by
name buys nothing the paragraph above it does not already say.
Three known gaps found while writing (unmapped tool names, single account,
anthropic-only OAuth) are recorded in TODO.md.
beforeStable.md P0 #5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
beforeStable.md is the 2026-09-05 audit against main at 360d1e5 (v0.29.0):
health checks, the five P0 blockers, six P1s, what is explicitly not a blocker,
and the path to 1.0. Each P0 now carries what was actually done and why, so the
reasoning survives the branch.
All five P0 items are closed. What remains before the tag is `pnpm eval:gate`
with FREECODE_JUDGE_PROVIDER set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
`tearDownIfEmpty` disposed a session's ring buffer the moment its last
subscriber left — which, for a single browser, is every disconnect. A
reconnect carrying Last-Event-ID then found no record and was told
`{ gap: false, events: [] }`: nothing was missed. Everything produced
while away was lost, without even a `stream_gap` marker. Strictly worse
than eviction, which at least admits the loss.
Record lifetime is now decoupled from subscriber count. The last leave
starts a 5-minute TTL (`emptySince`) that the existing reaper enforces;
a reconnect inside the window clears it and replays for real. Events
emitted while nobody is attached keep being buffered, as they already
were.
`replayForSubscriber` also stops lying when the record is genuinely
gone: a client that claims to have seen events but finds no buffer now
gets a gap, since we cannot know what it missed.
Tests: 4 in `web/stream-subscribers.test.ts` — replay across a full
disconnect, a gap once reaped, survival of a partial disconnect, and a
reconnect clearing the TTL. `runReaperForTests(now?)` is the seam; a
five-minute TTL is not assertable against a 15s unref'd interval.
Two failures that compounded in the same place — the summary that
replaces most of a long session.
**The brief was compacted first.** `selectForCompaction` preserved only
the tail and summarized `messages.slice(0, firstPreservedIndex)`, so the
oldest content went first and the first user message went with it. On
the next compaction the *summary of it* was summarized again: the
founding instruction decayed faster than anything else in the window,
which is the plausible mechanism behind long-session drift off the task.
The first user message is now carved out and preserved verbatim, after
tail-trimming so trimming can never evict it. A head over
`maxPreserveHeadTokens` (2k) is a pasted document rather than an
instruction and is summarized as before.
**The summary never saw the work.** A tool-calling turn was recorded as
the stub `[Executed N tools]`, so the transcript handed to the summarizer
contained none of the edits, commands or errors that were the actual
work — and `summarizer.ts`'s `extractToolCalls`/`extractFiles` matched
nothing, two paths that could not fire. `compaction/tool-transcript.ts`
renders one bounded line per call (`Tool <name>: <args> -> <outcome>`, in
the format the summarizer already greps for), and `addToolTurn` records
it.
The turn budget moved with it. `MemoryService.normalizeContent` is gone:
it clipped the *tail* of a long message, i.e. the most recent tools. The
transcript drops the oldest calls instead and says how many, which is the
right end to lose. `maxToolOutputChars` is now that turn budget (4k).
Tests: 6 in `tool-transcript.test.ts` (incl. one pinning the format
against the summarizer's own regexes), 2 in `selector.test.ts` (the brief
survives two compactions; an oversized head does not), and the two
existing service/pruning tests rewritten to the new invariants.
1234 pass / 0 fail.
`CLAUDE.md` calls `METHODS` the source of truth for the JSON-RPC surface.
It declared 25 of the 50 implemented handlers, so all of `memory.*`,
`config.*`, `models.*` and eight session ops had zero compile-time
checking in frontends — a wrong param shape was a runtime discovery.
`METHODS["session.send"]` also declared `StreamResponse` as its result,
which it has never returned: the handler resolves with the loop's result,
and per-token output goes over the stream channel. Its params were
missing `model`, `effort` and `agentMode` too.
All 25 are now declared, `session.send` is corrected, and
`ipc/methods-coverage.test.ts` asserts the two sets are equal in both
directions — adding a handler without declaring it fails the suite, which
is what makes the source-of-truth claim true rather than aspirational.
Six result shapes needed wire types, since `packages/shared` cannot import
from an app: `MemoryEntry`/`MemoryType`, `MemoryGraphStats`,
`RedactedConfig`, `TurnResult`, `ExportedSession` and `ModelInfo`. They
follow the existing `SessionMeta`/`SerializedMessage` mirror pattern, and
`ipc/wire-shapes.test.ts` pins each one with a type-level assignability
assertion so drift is a `check-types` failure rather than a field that
silently stops reaching frontends.
Two deliberate narrowings: `TurnResult` omits `LoopResult.finalState` (the
loop's internal state machine — no frontend reads it), and there is no
wire type for the raw config, only the redacted one.
1237 pass / 0 fail; check-types clean across all workspaces.
Every handler reads its params through `params as { … }`, a cast that
checks nothing. A missing or misspelled field arrived as `undefined`,
failed somewhere inside the handler, and came back as -32603 — an
internal error, which tells the caller the server is broken when the
request was.
`REQUIRED_PARAMS` in packages/shared declares, per method, the params a
handler genuinely cannot proceed without and their JSON types.
`handleRequest` checks it before dispatch and answers -32602 naming the
field and both types, so the call can be fixed from the error alone.
The table is typed `Record<MethodName, …>`, so a new method without an
entry is a compile error — there is no way to add a method that quietly
skips validation.
Scope is deliberately narrow. It validates presence and type of REQUIRED
params only: it is not a schema validator. Optional params stay the
handler's business (they have defaults), `null` counts as missing because
that is how a JSON caller usually spells "nothing" and it fails the same
way, and unknown params are ignored so a newer frontend still talks to an
older core. The rule for what goes in the table is what the handler
actually needs, not what `METHODS` types — `commands.list` types
`projectPath` as required but falls back to cwd, so validation must not
reject a call the handler would have served.
Tests: 9 in `ipc/validate-params.test.ts`, including the point of the
exercise — `handleRequest` returning -32602 rather than -32603 — plus one
that an unknown method is still -32601. 1246 pass / 0 fail.
`settings.json` is hand-edited, security-relevant, and had neither. Four
modules read it independently — permissions, hooks, memory, redirect —
and each ignores what it does not recognise. Right per reader; the sum
was that `"permission"` for `"permissions"` disabled every rule and
looked exactly like the permission system being broken.
`settings/known-keys.ts` is the one place that knows the whole shape, so
it can say "unknown key" and, for a near miss, which key was meant.
`settings/validate.ts` runs it over both scopes once per process, from
the shared hook bootstrap — the same bootstrap, because a second call
site is how `serve` and `run` diverged last time.
Deliberately a NAME check, not a schema validator. Values stay each
reader's business: they already validate and default their own, and a
second copy of those rules would diverge. Hook event names are left to
`hooks/settings.ts`, which already reports an unknown one with the full
valid list — one good message beats two in different words. And it warns
rather than refuses: a file with a stray key is still a usable file.
`schemas/settings.schema.json` is the editor half, wired up with
`$schema` (the one key FreeCode ignores on purpose). Two descriptions of
the same file drift the moment a key lands in only one, so a test asserts
the schema's key set matches `KNOWN_SETTINGS` section by section.
Tests: 9 in `settings/known-keys.test.ts`, including the motivating typo,
a section-level typo, the two deliberate non-warnings, and the
schema/runtime drift guard. 1255 pass / 0 fail.
Docs: `/reference/settings` gained the `$schema` section, `redirect` in
the top-level table (it was reachable but undocumented there), and the
four memory keys the page never listed —`retrievalJudge`,
`autoConsolidate`, `consolidateMinHours`, `consolidateMinSessions`. Two
Known-gaps bullets and their `TODO.md` entries are now closed.
All six P1 items are closed. Each entry says what was wrong, what was
built, and — where it matters more — what was deliberately not built: no
shared settings loader (the three-merge-rules gap stays open and is not
what made a typo invisible), no wire type for the raw config, no
validation stricter than the handler it guards.
Step 5 of the path to 1.0 is now the eval gate. P1 #2 and #3 both change
what the model sees after a compaction, so that run is the measurement,
not a formality.
I wrote that a moved judged suite would point at the compaction changes.
That is wrong: compaction fires at ~107k input tokens, the longest eval
prompt is ~65 tokens, and every case runs a single turn — so no case has
ever compacted or can. The transcript from P1 #3 reaches the model only
through the summary (loop.ts:1446 reads .summary, not recentMessages), so
with no compaction the prompt is byte-identical before and after.
The gate is a pre-tag regression check here, not a measurement. Recorded
the harness gap in TODO.md, with the cheap fix (a
FREECODE_COMPACT_TARGET_TOKENS override in the runner) noted over the
expensive one (multi-turn replay cases).
My TODO entry made this look like a new discovery. It is not: the harness
already tracks it in two places — CATEGORIES_WITHOUT_CASES in
dataset.test.ts and §9.1 of the case-registry spec, which states the
objection precisely (compacting is an accident today, and an accident is
not a p >= 0.99 case). A third copy in TODO.md would drift from both, so
that entry is now a pointer.
The idea itself belongs in §9.1 and is recorded there: the compaction
threshold is already a knob (FREECODE_COMPACT_TARGET_TOKENS via
getCompactTarget), so lowering it for one case turns compacting from an
accident into the point of the case. It needs a per-case env key on
EvalCase, which is harness work the spec says should be specified before
it is built — including whether arbitrary env is allowed (it should not
be) and how the gate compares a case that runs in a different
environment.
Not building it. Recording it where the decision will be made.
All eleven items (five P0, six P1) are closed and their outcomes live in the
commits that made them, so the audit file was a duplicate record with a
shelf life. `pnpm eval:gate` opened all three suites against
anthropic/claude-opus-5 — the last step it was tracking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
… temperament
The case exists to assert that the model reads before opining — its
`whyModelBacked` says so. What it actually blocked on was `expectMaxTurns: 6`,
a budget calibrated against minimax/MiniMax-M3, the only model it had ever run
on (18/18 passes). Its first run on claude-opus-5 came in at 9, 5 and 10 turns
and failed the majority.
The trace says that is thoroughness, not thrashing: 17 tool calls over 9 turns,
`repeatedCalls: 0`, no oscillation, monotonically widening from the named file
to its consumers to the tests and spec, ending in a real answer. The prompt
("what else would that affect") has no natural stopping point, so turn count
here measures how much a model explores, which differs per model — while the
gate's own regression rule is already per-model.
So: assert the sequencing directly with `expectFirstToolIn: ["read"]`, which is
model-independent, and raise the budget to 12 so it still catches a loop.
Note `expectMaxTurns` is checked before the tool assertions
(`scorers/trajectory.ts:29`), so the old failure never evaluated whether `read`
fired first — the trace confirms it did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy5ynPzdM4gJ8o1qKpp5uX
@vercel

vercelBot commented Sep 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated
freecode-docsReadyReadyPreviewSep 5, 2026 1:34pm UTC
freecode-webReadyReadyPreviewSep 5, 2026 1:34pm UTC

@ayan-de
ayan-de merged commit abee2bb into mainSep 5, 2026
4 checks passed
Sign up for freeto 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

@ayan-de