Still Can Work

What's new

Every improvement to Still Can Work, most recent first.

All notable changes to Still Can Work are documented here.

Format follows Keep a Changelog; versioning follows SemVer.

0.26.1 - 2026-10-04

Added

open** (AB#2438, AB#2439, AB#2468). No app code changes.

`/rebuild?auto=1`, "Re-check my corrected resume", and editing the recruiter

resume inline (preview, copy, PDF text, print, hidden blank logistics). AI

routes are stubbed so it is deterministic; `PW_MODULE` points it at a local

Playwright install, which is not a dependency.

reaches the handler. No code, a wrong-secret code, an expired code or a

tampered code each get 401 `access_required`. `/api/unlock` stays open.

0.26.0 - 2026-10-04

Added

(AB#10522). Every ATS issue, age-bias flag, gap and line upgrade now has an

**Ignore** control with a reason - *It's inaccurate* or *I'm OK with this*.

Ignored items move to an **Ignored (n)** panel (Undo, Clear all), are left out

of "Apply these upgrades" and the recruiter resume, and are sent with every

later review so the model neither repeats them nor docks the score for them.

**Re-score without these** re-runs the review on the stored resume and shows

each score's before -> after. The list lives in this browser only

(`scw.ignored`), survives "Analyze a different resume" (a revised upload is

exactly when the same advice used to return), and is cleared by Clear all or

the workspace reset. `/api/analyze` takes an optional `ignored` field: a wrong

shape is a 400, over-long text and over-long lists are clamped, never rejected

(`lib/ignored.ts`, `docs/resume-review-ignore.md`).

0.25.7 - 2026-09-30

Security

(remote code execution in `next/og` `ImageResponse`; affected `>=16.2.0 <16.3.6`).

This app does not use `next/og` or `ImageResponse`, so exposure was lower, but the

dependency was in the vulnerable range. `eslint-config-next` moves to match.

(GHSA-3wwx-pv8p-q78v, GHSA-r53p-7pc4-xj5r, GHSA-rfgv-xxqx-mfg5 and others) and

`js-yaml` (GHSA-2883-xcg3-v3hh). `npm audit` now reports 0 vulnerabilities

(was 1 critical, 3 high).

0.25.6 - 2026-09-12

Fixed

all along and passes locally, but no workflow ever invoked it, so a

regression could land on `main` unnoticed. A new `quality` workflow builds

and runs the (fully mocked, credential-free) suite on every pull request

and push to `main`. Wiring it in surfaced two real bugs: the script itself

(`node --test "tests/**/*.test.mjs"`) only worked on newer Node versions,

fixed by switching to plain `node --test`; and every `tests/*.test.mjs`

file imports its subject straight from `lib/*.ts`, relying on Node's

native TypeScript type stripping, which the Node 20 this repo's other

workflows pin does not have — the `quality` workflow pins Node 24 instead.

dependency-scan workflow of any kind. A new `dependency-scan` workflow runs

`npm audit --omit=dev --audit-level=high` on a Monday schedule and on

demand (`workflow_dispatch`), so a newly-published advisory against an

unchanged dependency is caught even between PRs. It is not a required PR

check.

0.25.5 - 2026-09-12

Docs

(AB#5186). The `ChAImpOrg` GitHub organization was deleted; the gitleaks

workflow comment and the README deploy instructions named it, and now point

at `ChAImpNPO/stillcanwork`.

0.25.4 - 2026-09-11

Fixed

feedback carries roles as objects** (AB#3674). `/api/analyze` returns

`suggestedRoles` as `{ title, why, ... }`; the pages map them to titles before

posting, but a payload that carried the objects through hit the route's schema

and was answered - and recorded by self-healing - as a server failure. Both

routes now accept either shape and keep the title (`lib/feedback.ts`), and a

form field that is still malformed is the caller's 400, not a recorded 500.

0.25.3 - 2026-09-09

Fixed

`ab-link` carried `if: !endsWith(github.actor, '[bot]')` at job level, but

`github.actor` is whoever triggered the current event, not who authored the PR —

so the Azure Boards integration's own `pull_request: edited` run matched the

exemption and skipped. A skipped job reports `neutral`, which counts as passing

and supersedes the real run. The job no longer carries a job-level `if:` at all:

it always runs and always reports a real conclusion, and the bot exemption moved

into the step keyed on the PR **author**. The rules moved out of YAML into

`scripts/ab-link-check.mjs`.

0.25.2 - 2026-09-09

Security

critical advisories: GHSA-2xp9-vwfh-vxw4 (CVSS 9.5, unauthenticated RCE in the

Image Optimization API when AVIF files are used) and GHSA-p293-qw3h-jr36

(CVSS 9.0, unauthenticated RCE on Windows-hosted servers). The

image-optimization path is reachable by anyone who can request an image URL.

`eslint-config-next` moved with it.

through `overrides` since it is transitive.

This repo runs no dependency scanner at all, so nothing here would have reported

either advisory. Found by scanning the estate's lockfiles directly; the coverage

gap is tracked on AB#4935.

0.25.1 - 2026-09-02

Fixed

(AB#3649). `/api/usage` was sent with `stale-while-revalidate=600`, so the CDN kept

serving the first (empty) response long after money had been spent - a figure that is

merely late is fine, one that reads zero looks broken. Now `max-age=30,

stale-while-revalidate=30`.

slower with each day recorded - the long CDN cache had been hiding it. Added a

60-second in-instance memo. The remaining limit is written down rather than

speculatively engineered: past days never change, so when history makes this slow the

fix is a rolled-up total for closed days with only today's files read live.

0.25.0 - 2026-09-01

Added

usage and cost, and the footer shows the **cumulative** spend across all visitors -

"what it takes to keep this running", not any individual's usage. Nothing per-visitor

is stored, so no one's spend is identifiable.

table on 2026-09-01: Sonnet 5 $2/$10 per 1M in/out, Haiku 4.5 $1/$5.

guessed one**, and the count of unpriced calls is reported alongside the total so it

is never read as complete when it isn't.

gated on `FEEDBACK_ADMIN_KEY`, the same key as the feedback listing.

other; within an instance writes are serialised. A shared counter would silently

lose increments, and a total that undercounts is worse than none.

exactly what made the incident pipeline silently dead (AB#3455), three consecutive

failures raise `USAGE_STORE_UNAVAILABLE` saying the displayed figure is now an

undercount.

Changed

a real working session rather than a round number. One application costs 3-4 calls, so

a 20-applications-a-day target is 60-80 calls a day - but the window is ten minutes, so

the burst matters, not the daily total. Each call takes 20-60s, so one person working

sequentially can only issue ~10-20 calls in that window; the old cap of 15 sat exactly

on that ceiling and throttled genuine use. The per-instance wallet guard (120/min) is

unchanged, so total spend exposure does not rise.

0.24.2 - 2026-09-01

Fixed

`ADO_AUTOFILE_PARENT` defaulted to `768`, above a comment claiming that was "the

StillCanWork feature". It is not: 768 is `B1 - Automatic issue capture` in

`One\Platform\Portal`. The number came from the implementation this was modelled on and

was never updated. An existing test asserted `.../workItems/768`, so the defect was

locked in as expected behaviour.

There is now **no default**. An unset or invalid parent makes the filer refuse to file

and log `INCIDENT_FILING_UNCONFIGURED`, and `buildBugOps` omits the parent relation

rather than fabricating one - a Bug attached to another product's Feature reads as

tracked while being absent from every plan, which is harder to notice than nothing

being filed at all.

Configuration (done outside this repo)

unset the daily `50 12 * * *` filing job had been rejected on every single run - the

reason no incident has ever been filed.

Still blocked

the federated `ADO_FILER_CLIENT_ID` + `AZURE_TENANT_ID` app registration. None is set,

and a credential cannot be created from here. Until one exists, incidents are captured

and stamped but never filed.

0.24.1 - 2026-09-01

Fixed

the real page in a browser: the fit result plainly carried `City of Tacoma`, the letter

named the role, opened "Dear Hiring Manager," and never said who it was to - which reads

as a mass mailing. Four earlier API runs happened to include the employer, so the

end-to-end check passed and the defect hid behind it. The prompt now requires the

organisation to be named in the body at least once, the E2E asserts the employer

separately from the placeholder check, and a unit test guards the instruction against

being lost in a reword.

0.24.0 - 2026-09-01

Added

a "Write my cover letter" action produces a ready-to-send letter built from what the

analysis actually found: matched requirements supply evidence-backed claims,

`atsKeywords` the posting's literal screening vocabulary, and `talkingPoints` the

opening hook. Previously the only cover letter lived on `/jobs`, was written from a

job-board snippet, and knew nothing about a fit check.

a credible mitigation are pre-empted, one sentence each. High-severity gaps are never

mentioned - a letter that volunteers a disqualifying weakness argues its writer out of

an interview, and this audience is already fighting an unfair screen.

forbids inventing an employer, title, date or credential.

Nothing is stored server-side.

Without it the route behaves exactly as before, so the `/jobs` flow is unchanged.

Inbound `fit` is clamped, never rejected (the AB#2946 lesson): over-long strings are

truncated, surplus and unusable entries dropped, and a non-boolean `have` coerces to

`false` so a keyword is never wrongly claimed.

0.23.2 - 2026-09-01

Fixed

`process.env.npm_package_version` is only set for a process started by an npm script;

Vercel's runtime does not start handlers that way, so it was undefined in every

deployment. Triage could not tell releases apart - precisely the information that was

needed while AB#2946, AB#3454 and AB#3494 were reaching users.

`next.config.ts` now injects `APP_VERSION` at build time from `package.json`, keeping

it the single place a version is written, and the new `lib/version.ts` exposes it with

an `npm_package_version` fallback for `npm run dev`. The four call sites

(`/api/health`, `/api/feedback`, `lib/incidents.ts`, `lib/incident-intake.ts`) use it.

A test fails if anything reads `npm_package_version` directly again, and another

asserts the changelog's top version matches `package.json`.

0.23.1 - 2026-09-01

Fixed

(AB#3494). `generateText()` resolves even when the model produced nothing usable;

the failure comes from the `output` **accessor**, which the AI SDK documents as

`@throws NoOutputGeneratedError`. Every `Output.object` route was written as

`const { output } = await withModelFallback(...)`, which reads that accessor

**after** the fallback has returned - so the chain recorded a success, returned, and

the getter threw straight past the `catch` that exists to try the next model.

New `withModelFallbackOutput()` reads `output` **inside** the callback, so the throw

lands where the fallback can see it. All 10 call sites converted (`analyze`, `fit`,

`interview`, `jobs`, `know-me`, `move`, `photo`, `rebuild`, `recruiter-resume`,

`linkedin`); `cover-letter` reads `text`, a plain property, and is unchanged.

A test walks `app/api/*/route.ts` and fails if any route destructures `{ output }`

again - the trap is silent when reintroduced, so it is guarded structurally.

**This is why 0.22.2 (AB#3454) did not fix the symptom.** Detecting the error by

class instead of by AI SDK v5 message text was necessary and correct, but the error

never reached the predicate. Production logs confirmed it: `/api/rebuild` 500s on the

deployment containing that fix, with no `skipping` line logged at all.

0.23.0 - 2026-09-01

Fixed

(AB#3455). Two independent faults, both dating from the project rebuild:

1. `BLOB_READ_WRITE_TOKEN` on the Vercel project held the **`FEEDBACK_ADMIN_KEY`

value** - a 48-character key with no `vercel_blob_rw_` prefix - so every Blob

call failed with `Invalid token: unable to extract store ID`. The store was also

connected to **zero** projects, so nothing was being injected. Fixed in Vercel:

the bogus variable was removed and the `stillcanwork-feedback` store is now

properly connected to the project.

2. Incidents read and write with `access: "private"`, but a Blob store's access is

**fixed when the store is created**, and `stillcanwork-feedback` is public - so

private operations answered `400 Bad Request` and could never have worked.

Incidents now use a dedicated **private** store (`stillcanwork-incidents`) via

`INCIDENTS_READ_WRITE_TOKEN`, falling back to `BLOB_READ_WRITE_TOKEN`. Public was

not an option: the path is `incidents/<signature>.json` with

`addRandomSuffix: false` and the signature derives from the error, so a public

URL would be guessable by anyone able to reproduce the error.

Added

a configuration reason (bad token, wrong store access, denied access), a distinct

error is logged **once per instance**, in the same shape as AB#2422's

`AI_PROVIDER_MISCONFIGURED`. A self-healing pipeline that silently captures nothing

is worse than none, because it is trusted: AB#2946 and AB#3454 both reached users

while this pipeline was dead and mute. One-off blob errors are still warned about

per incident but are not escalated.

0.22.2 - 2026-09-01

Fixed

answer instead of falling back** (AB#3454) - `isSkippable()` in `lib/ai.ts` detected an

unparseable model answer by message text, matching the **AI SDK v5** wording

(`"No object generated"`). The repo runs `ai@^7.0.77`, which throws

`NoOutputGeneratedError` with the message `"No output generated."`, so the predicate

never matched: one bad answer threw straight out of `withModelFallback` with 20 chain

entries left untried, and the route answered 500. Detection is now by **class and error

name** (`NoOutputGeneratedError.isInstance`, `NoObjectGeneratedError.isInstance`, plus a

name allow-list), never by message text. The predicate also walks the whole error chain

(`lastError`, `cause`, `errors[]`, cycle-safe) rather than following `lastError` alone,

so a provider failure buried three levels down still routes correctly. New

`unusableOutputReason()` reports `finishReason` (`length` = truncation) in the skip log.

`new Function` - it imports `isBusyError` directly. The old form failed on any syntax

change to the file, which is a test that breaks on refactors rather than on regressions.

Known broken, not fixed here

(AB#2593) discards every incident and `GET /api/feedback` answers 500. Both fail with

`Invalid token: unable to extract store ID`. This needs the Blob store reconnected to

`prj_hcm7yoUTunY6JEwcIgB4rQ8UZEPH` in Vercel - a project-configuration change, not a

code change. It is why AB#2946 and AB#3454 both went unreported by the pipeline built

to catch them.

0.22.1 - 2026-08-31

Fixed

ran long** (AB#TBD) - `careerContextSchema` capped `workPreferences` at 160

characters and `watchOutFor` at 200, but `/api/know-me` generates those

strings with no cap at all. One long sentence from the model made the context

stored on the visitor's device permanently unparseable, so `/api/jobs`,

`/api/fit`, `/api/cover-letter`, `/api/interview`, `/api/move`, and

`/api/linkedin` answered 500 on every later request from that browser - the

only escape was clearing the workspace. Inbound career context is now

**clamped, never rejected**: over-long strings are truncated (limits raised to

2000/400), surplus and non-string list entries are dropped, and a malformed

context degrades to "no context shared" instead of failing the request.

New `parseCareerContext()` covers the multipart routes; `careerContextField`

covers the JSON ones.

`npm test` never globbed, so it had not been running in CI.

0.22.0 - 2026-08-28

Added

no-database incident pipeline. Uncaught server errors (`instrumentation.ts`

`onRequestError`), handled 5xx responses on every API route (`reportHandled`), browser

errors/unhandled rejections, React render errors (`app/error.tsx`), and 5xx answers to

same-origin `/api/*` calls (fetch hook, 5 reports per page load) are scrubbed at write

(emails, phones, SSNs, JWTs, hex, UUIDs, file paths), deduped by signature into

**private** Vercel Blobs at `incidents/<signature>.json`, and filed daily as one

anonymized ADO Bug per new signature (`/api/cron/incident-ado-filing`, `CRON_SECRET`-gated,

Entra-first via Vercel OIDC with PAT fallback, clean no-op when unconfigured). New

unauthenticated intake `POST /api/incident-report` (honeypot, per-IP cap, always 204,

`GET` → 405) is exempt from the access gate. 4xx/429 never record; nothing identifying

is ever stored. See `docs/self-healing.md`.

0.21.0 - 2026-08-28

Added

tool can be reserved for people navigating a tech layoff, reached through the

communities supporting them. Partners hand out **stateless HMAC access codes**

(`lib/access.ts`); a valid code unlocks the app and the middleware enforces it on

the AI routes. No account, no name, no database — the code carries no identity and

nothing about the person is stored. Ships **off**: it activates only when

`ACCESS_CODE_SECRET` (server) and `NEXT_PUBLIC_ACCESS_GATE=1` (client) are set. New

`POST /api/unlock`, `middleware.ts`, `components/AccessGate.tsx`, and

`scripts/gen-access-codes.mjs`. See `docs/access-codes.md`.

0.20.1 - 2026-08-28

Added

work-item reference; near-misses fail with an explanation; auto-close

keywords rejected; bots exempt. Rollout of AB#823 from KR.

0.20.0 - 2026-08-28

Added

(AB#2422).** A 401 on the configured direct Anthropic key records a distinct

`AI_PROVIDER_MISCONFIGURED` error (once per instance) while the fallback

chain keeps serving users; a successful direct call heals the state.

state — no AI call); `?deep=1` with the admin bearer key runs a minimal

live ping so an invalid key is caught by monitoring, not by a user.

Capacity throttling still degrades to the friendly busy message; auth

failures are never classified as busy.

0.19.0 - 2026-08-27

Added

button that rebuilds your whole resume tuned to that specific role — leading with

what matches, mirroring the posting's wording, weaving in the tailored bullets and

screening keywords where your real experience supports them, and addressing the

gaps honestly. Nothing invented.

the resume review now takes a detail you can add ("actually, I did a QuickBooks

course last year") — and the resume builder folds it in as a real fact when you

rebuild.

0.18.0 - 2026-08-27

Added

Canada, Ireland, Europe, Australia, NZ, remote-anywhere). Pick where you want to

work and on-site roles outside those regions are scored well down — remote roles

still count. Fixes results skewing heavily European for people elsewhere.

more scrolling past 18%-fit roles.

in your tracker (applied or rejected) from the matches.

records that the employer has gone quiet. It sets a "Not progressing" status in your

tracker (also a new status option there) and feeds an anonymous learning signal.

0.17.0 - 2026-08-27

Added

and the line-by-line upgrades section of the resume review each get a button that

rebuilds your resume with those fixes applied — plus the age-bias repairs and

stronger wording — in a single click, nothing invented.

the honest review on the corrected resume so your robot-proof and readiness scores

update with every fix factored in.

produced recruiter resume into a full editable form — every field, including each

role's bullets (with add/remove role) and the recruiter note — so you can adjust

the wording before downloading the PDF, copying, or printing.

0.16.1 - 2026-08-27

Added

button on the recruiter resume produces a clean, single-column PDF file you can

email or attach to a LinkedIn message — a real file, not a browser print. Built

server-side with `pdf-lib` (`POST /api/recruiter-pdf`). Copy-as-text and Print

remain.

0.16.0 - 2026-08-27

Added

recruitment consultant — the person who decides who to put in front of their

clients. It leads with a pitch-ready positioning headline and summary a recruiter

can lift verbatim, states target roles and the level to market at, frames

experience as sellable outcomes, and lists selling points. An optional details

form captures the logistics a recruiter needs — availability, location, work

preference, work authorization, and (only if you enter it) salary expectation —

and nothing is invented: blanks stay blank. It also generates a short intro note

you can paste into an email to a recruiter. Reachable from the analysis results

("🤝 Recruiter-ready version") and the More menu. New `POST /api/recruiter-resume`.

0.15.1 - 2026-08-24

Changed

0.15.0 - 2026-08-24

Added

0.14.2 - 2026-08-24

Fixed

0.14.1 - 2026-08-24

Changed

0.14.0 - 2026-08-24

Added

0.13.0 - 2026-08-24

Added

Changed

Fixed

0.12.0 - 2026-08-24

Added

0.11.0 - 2026-08-24

Added

0.10.0 - 2026-08-24

Added

0.9.0 - 2026-08-24

Added

0.8.0 - 2026-08-23

Added

0.7.1 - 2026-08-23

Fixed

0.7.0 - 2026-08-23

Added

0.6.0 - 2026-08-23

Added

0.5.0 - 2026-08-23

Added

0.4.0 - 2026-08-23

Added

0.3.0 - 2026-08-23

Added

Changed

0.2.0 - 2026-08-23

Added

0.1.1 - 2026-08-23

Fixed

0.1.0 - 2026-08-23

Added