`SectionItemHeader` only rendered its own box when a template opted into
`mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the
header children and attached the resolved `item-header` style to the first
descendant that happened to be a literal `View` or `InlineItemHeader`.
Sections whose header starts with anything else — certifications, awards,
projects, publications, references — matched nothing, so the style was
silently dropped; stacked headers such as experience matched only their
first row, so a second row went unstyled.
The header now always renders its own `Div`, so `item-header` covers the
whole header row of every section on every template. `Div` rather than
`View` keeps the base row gap the rows used to inherit from the item box,
and Ditgar keeps its tight header via `rowGap: 0` on its own
`sectionItemHeader` slot, so rendered output is unchanged apart from the
newly styled rows. `mainItemHeaderBorder` is now dead and removed.
Fixes#3349
* docs: remove .superpowers
* feat(dsh-plugin): bring the DeepSeek Harness plugin into the monorepo
Moves dsh-plugin-reactive-resume out of its own repository and into
packages/dsh-plugin. It stays a published, public npm package — the only
one here — but now builds, typechecks, tests, and lints under the same
turbo tasks as everything else.
The move pays for itself in the drift guard. Standalone, the plugin kept a
generated snapshot of the tool names scraped from the live server card at
https://rxresu.me, plus a weekly CI job to notice when that snapshot went
stale. Sitting next to packages/mcp, it reads MCP_TOOL_NAME directly, so a
tool rename breaks the prompt guide on the same pull request instead of
days later. The snapshot, the fetch script, and the scheduled job are gone.
packages/mcp gains a ./tool-names export so that import goes through the
public export map rather than another workspace's src.
Also flips autoInstallPeers off. The DeepSeek Harness rc packages declare
peers that are host-supplied and, in one case
(@deepseek-ai/dsh-type-meta), not published at all, so auto-install 404s
the whole workspace. Turning it off drops only optional peers elsewhere;
@neodrag/core was the single hard peer that had been arriving implicitly,
and it is now declared where it is used. Full typecheck and test suites
pass, and pnpm peers check reports nothing new beyond the pre-existing
drizzle-orm range mismatch.
Tests move from test/ to colocated src/*.test.ts and the build output from
lib/ to dist/ to match repository conventions.
* fix(dsh-plugin): ship a bundle manifest and target the current Harness
`dsh plugin add` warned that the package "declares no dsh.bundle — installed
as a plain dependency, not a profile layer", and it was right. Every other
Harness plugin, in-box and third-party, ships a cordis.patch.yml and points
dsh.bundle.patch at it; that declaration is what joins a package to a
profile's bundle stack. Without it the package installed and then sat inert,
and the README's hand-written insert row was a workaround for the gap rather
than the intended way in.
The peer ranges were also a generation behind. They asked for
@deepseek-ai/dsh-mcp-client and dsh-system-prompt at ^0.0.1-rc.1, which
cannot match the 0.1.0-rc.6 a current harness ships, so the plugin could
never have resolved against the thing it targets. Both APIs are unchanged
across the bump — StreamableHttpConfig still takes the same six fields and
PromptSection still takes name/order/text — so this is a range correction,
not a migration.
That bump pays for itself elsewhere. The old generation peer-depended on
@deepseek-ai/dsh-type-meta, which was never published, and working around
that 404 is why merging this package turned autoInstallPeers off for the
whole repository and pulled @neodrag/core in by hand. The new generation
dropped that peer and publishes every other one, so both changes are
reverted and pnpm-workspace.yaml is back to what it was.
Because a bundle patch mounts the plugin the moment it is installed, a
required apiKey would fail config validation and take the profile down
before the user ever had a chance to mint a key. It now defaults to empty
and apply() warns and mounts nothing, matching how dsh-honcho-memory
handles the same problem.
Verified by packing the tarball and installing it into a clean project with
default pnpm settings: it resolves, imports, and reports its exports.
context.issue spreads to { owner, repo, number }, but Octokit v9 requires
issue_number. The request hit /repos/.../issues//labels and returned 404,
so no opened issue was ever labeled.
Section item headers render the title and its trailing date inside a shared
split row styled with `flex-wrap: wrap`. When the title is long the date wraps
onto its own line and left-aligns instead of staying pinned right, which reads
as inconsistent down a list of certifications.
That row had no selector. It is a bare View, so it never reached the semantic
tree: `item-header` matches the box around the row, and the title and date are
text nodes that layout properties do not apply to. There was no stylesheet that
could reach it.
Expose it as `template-part[name="item-header-row"]`, shared by every template
because the row comes from the shared section components. Awards,
certifications, projects and publications are covered; experience, education
and volunteer stack two rows and hand their headers to the
`inline-item-header-*` parts on some templates, so they are left alone.
Readers can now write:
@version 1;
template-part[name="item-header-row"] { flex-wrap: nowrap; }
The sonner wrapper was the only consumer of next-themes; the Base UI toast
that replaced it does not use the hook. knip flagged it as unused, and CI's
`knip --fix` step removed it and then failed `pnpm check` against a lockfile
that still listed it.
Fills the 25 strings added this cycle by the toast migration, the account page
rename, the Custom Styles status labels and the job posting auto-fill, across
all 53 target catalogs. The zu-ZA pseudo-locale is intentionally left empty.
Adds the questionnaire and empty-state primitives and renders the
ask_user_question tool call inline in the chat, so the agent can offer choices
instead of guessing when a request is ambiguous.
Fetching an arbitrary job URL server side meant owning SSRF defence, redirect
and size limits, and per-site scraping quirks. The autofill tool now takes only
pasted text, so the URL input, the fetch path and its MCP annotation are gone.
The sheet gates the call behind a tested AI provider and a minimum paste length
so a stray snippet does not spend an AI call.
The page now holds account-level actions rather than only destructive ones, so
it is reachable at /dashboard/settings/account and presented with a neutral
icon in the sidebar and command palette.
Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
Turbo defaults to ten concurrent tasks and each vitest sizes its pool to the
core count, so a ten-core machine ran roughly a hundred workers and a 1.6s test
blew its 15s budget. Different suites failed on every run. At concurrency four
the whole repo passed five runs straight with no wall-clock cost.
Each case rebuilt the whole spec, which walks every router and resume JSON
schema. The first case already carried a raised 15s timeout and still timed out
on a loaded machine. The spec is deterministic and only read here, so build it
once: the file drops from over 15s to 1.86s.
Without isolation the files in a worker share one module registry, so a
vi.mock of @reactive-resume/env/server in one file leaked into another and
whichever file imported the module first won. Measured on a clean cache,
isolate: false failed four of four whole-repo runs; with isolation, none.
Units that transitively import the validated server env threw at import time
whenever no .env was present, taking out packages/auth and packages/api. Seeding
the three required variables in the shared setup fixes every current and future
caller in one place. Real values still win.
The case passed cursor position 5 into "--resume-", which lands mid-token and
reads as a selector context, so it received the selector list. Every other case
in the file uses source.length.
The expected offset disagreed with its own line and column: line 2 column 17 is
offset 28, which is where `red` starts. Offset 31 pointed at `; }`. The sibling
UTF-16 case in the same file already used the correct arithmetic.
Anchors the offset to the source it must point at so it cannot drift again.
Disabling the textarea for the duration of a response made the browser blur it,
so the caret left the composer on every send and had to be clicked back. send()
already ignores calls mid-stream, so Enter stays a no-op and type-ahead works.
Every step-start part serialises to the same JSON, so the content-derived key
collided for any multi-step assistant message and React warned about duplicate
keys on each incoming chunk. Two identical text parts collided the same way.
Parts are append-only and never reordered by the AI SDK, so the index is stable.
Public resume pages only produced their OpenGraph and Twitter tags client side,
so a shared link had no card at all. The server now injects them into the shell
and swaps in the resume's own title and description.
The lookup is scoped to public, password-free resumes and deliberately avoids
resumeService.getBySlug: that counts a view and would expose a protected
resume's summary to an unauthenticated crawler. User-authored values are escaped
before they reach the HTML, and any lookup failure falls back to the plain shell.
getResumeSocialMeta is shared with the client route head so the two cannot drift.
X reads twitter:* meta tags from the name attribute, not property, so the card
validator reported twitter:title and twitter:description as missing. Also adds
the og:type tag the root head was never emitting.
The static middleware was mounted ahead of the web app fallback, and Hono's
serveStatic resolves "/" to the directory and returns dist/index.html verbatim.
handleWebApp never ran for the root route, so the OpenGraph, Twitter, canonical
and JSON-LD markup it injects was missing in production - fetching
https://rxresu.me/ as Twitterbot returned zero og: tags.
Route "/" explicitly before the static middleware so the injection runs.
* feat(resume): add a deterministic ATS parseability check
Adds an offline ATS linter that reports whether a parser can read a
resume, surfaced as an always-on panel in the builder.
The existing Resume Analysis panel needs a configured AI provider, so
users who never set one up get no feedback at all. These 22 rules run
as a pure function over ResumeData with no provider, no network and no
rendered PDF, so they work for everyone on every edit.
Rules cover contact details, date parseability, sections that hold
content but never render, column and sidebar placement, and typography
thresholds. The catalog mirrors the Semantic CSS diagnostic catalog:
stable codes carrying a severity, meaning and action, with no i18n
dependency so the web layer translates by code. Each finding carries a
JSON Pointer, which is what makes jump-to-field work.
Deliberately no second score. Resume Analysis owns overallScore, so
this reports "N of M checks passed" and counts by severity instead.
* fix(resume): accept localized ongoing periods and reject bare ones
Two period-parsing bugs found in review.
The ongoing-token set was English-only, so a German resume reading
"2020 - heute" was reported as unparseable and the panel told the user
to rewrite a perfectly valid range. Rather than guess translations for
55 locales, a range ending that carries no digits and is not a month
name in the resume's locale is now read as ongoing. That keeps a
genuinely incomplete ending such as "Jan 2020 - Feb" reported, since
"Feb" resolves as a month.
A bare "Present" also parsed as a valid period, so an experience entry
with no start date passed the check. A standalone ongoing token is now
rejected; ongoing tokens remain valid as the end of a range.
* feat(web): scroll ATS findings to the item they belong to
Findings for different items in one section all landed on the section
header, so a date problem on the third role gave no more help than
naming the section.
getAtsFindingTarget now resolves the offending item from the finding's
JSON Pointer against the resume, and SectionItem carries a matching DOM
id. The panel scrolls to that item and falls back to the section header
when the item is not mounted, which is what happens while its section
is collapsed.
* fix(resume): recognize ongoing periods by token, not by shape
The previous heuristic read any short, digit-free range ending as an
ongoing marker, so "2020 - unknown", "2020 - later" and "2020 - tbd"
parsed cleanly and suppressed the finding they should have raised.
Replaced with an explicit table of ongoing words keyed by language,
covering the locales the app ships. Matching is exact, so unrecognized
endings are reported again. A locale missing from the table falls back
to the earlier behaviour of reporting its ongoing periods, which is a
visible gap someone can close by adding a word rather than a silent
hole in detection.
Tests assert every listed token parses and that the table stays
lowercase, since lookups normalize that way.
* fix(ats): parse punctuated ongoing tokens
* fix(ats): parse Unicode punctuated ongoing tokens
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* docs(agents): align Redis compose commands with development guide
- Problem: AGENTS.md omitted Redis from dev infrastructure compose commands
while docs/contributing/development.mdx starts redis for local dev.
- Fix: document full postgres/redis/seaweedfs compose command and note that
REDIS_URL and ENCRYPTION_SECRET are required for AI agent features.
- Verification: preflight upstream fetch; manual diff against development.mdx
and compose.dev.yml redis service; duplicate PR gate passed.
* fix(pdf): ignore phantom gengar skill text nodes
- Problem: gengar template resumes with skills keywords fail semantic CSS activation because the legacy renderer emits a harmless empty text node that parity treats as a mismatch.
- Fix: treat the specific empty text artifact as presentation-neutral in the legacy parity comparator and add a regression test for the phantom fontSize 9 node.
- Verification: pnpm test src/semantic/legacy-parity.test.ts in packages/pdf passed (31 tests).
* docs: clarify host and container Redis URLs
- Problem: the development guide only showed the Docker Redis hostname, which fails for host-run development.\n- Fix: document localhost for host execution and redis for Docker execution.\n- Verification: pnpm test src/semantic/legacy-parity.test.ts (31 passed).
* fix(pdf): omit empty skill proficiency text
* test(pdf): cover blank skill proficiency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(import): auto-detect JSON format and show readable errors (#1)
Readable import errors, a fail-soft v4 parser, and auto-detect of the JSON format so uploads just work. The format dropdown becomes an optional override. PDF and DOCX (AI) paths are untouched.
* fix(import): address review feedback on the v4 guard and error message
- reactive-resume-v4-json.tsx: reject arrays in isRecord so array-valued
basics, sections, or metadata no longer pass the v4 shape guard.
- reactive-resume-v4-json.tsx: reuse the guard's error instance in the
catch arm instead of allocating a duplicate NOT_V4_MESSAGE.
- error.ts: use a singular "Problem" label for root-level Zod issues so
the message stays grammatical.
- add a regression test for array-valued v4 branches.
* fix(import): preserve selected JSON format
* test(import): cover selected JSON parser
---------
Co-authored-by: MrTig-afk <MrTig-afk@users.noreply.github.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
react-zoom-pan-pinch calls preventDefault() on its window-level mousedown
listener so that dragging the canvas does not select text. That also cancels the
browser's native focus shift, so focus stays wherever it was before the pan --
typically the sidebar button that opened the last dialog, since closing a dialog
restores focus to its trigger. A focused button activates on Space keyup, so
holding space to pan and then releasing it reopened the most recent dialog.
Blur the focused element from onPanningStart, which reinstates exactly the focus
change the browser would have made on its own. onPanningStart only fires when the
mousedown target is inside the transform wrapper, so sidebar and dialog clicks are
unaffected, and keyboard-only users never trigger a pointer pan.
Closes#3300
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
The icon picker grid starts with an empty string entry that renders the
"no icon" (prohibit) symbol, but the onClick guard "if (icon)" treated
the empty string as falsy and ignored the click. Change the guard to
check for a defined string value so the empty/no-icon option can be
selected.
Closes#3252Closes#3261
Co-authored-by: Devin <devin@example.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>