Skip to content

chore(deps): upgrade all dependencies (0.22.8) - #325

Merged
ralyodio merged 2 commits into
masterfrom
chore/deps-2026-10-26
Oct 26, 2026
Merged

ralyodio merged 2 commits into
masterfrom
chore/deps-2026-10-26

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Fleet dependency upgrade.

  • No Next.js in this repo (Hono server).
  • Majors taken: @hono/node-server 1 -> 2, typescript 5.9 -> 7.0 (typecheck clean), @profullstack/hqtui 0.5 -> 0.11.1
  • Also: x402-gateway 0.13.1, profile 0.1.6, synconfig 0.1.3, footer 0.3.0 (spec stays latest), hono 4.13.14, pg 8.23.1, @simplewebauthn/server 14.0.3, prettier 3.9.10, @types/pg 8.23.1
  • Held: @types/node on 24.x (24.19.2) to match the node:24 build stage
  • Bun runtime image 1.4.0 -> 1.4.4 (node:24-slim and bookworm-slim unchanged)
  • Version 0.22.7 -> 0.22.8 (package.json + src/config.ts). There is no publish workflow, so npm is not published here.
  • Audit: 0 critical / 0 high both before and after (1 moderate before, 0 after)

Local gate: typecheck, build + node --test 890/890. Boot test: built the production image and ran it against a throwaway postgres:17 (all migrations applied): / 200, /api/v1/stats 200, /signup 200, /api/v1/me/profile 401, running 70 s+ with no errors.

🤖 Generated with Claude Code

@hono/node-server 2, typescript 7, hqtui 0.11.1, x402-gateway 0.13.1, profile
0.1.6, synconfig 0.1.3, hono 4.13.14, pg 8.23.1, simplewebauthn 14.0.3. Bun
runtime image 1.4.4. Boot-tested with a throwaway Postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Priority Alert  (click "▶" to expand/collapse) Action
Low priority
License policy violation: npm @typescript/typescript-aix-ppc64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-aix-ppc64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-aix-ppc64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-darwin-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-darwin-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-darwin-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-darwin-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-darwin-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-darwin-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-freebsd-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-freebsd-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-freebsd-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-freebsd-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-freebsd-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-freebsd-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-arm under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-arm@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-arm@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-loong64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-loong64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-loong64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-mips64el under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-mips64el@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-mips64el@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-ppc64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-ppc64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-ppc64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-riscv64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-riscv64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-riscv64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-s390x under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-s390x@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-s390x@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-linux-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-linux-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-linux-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-netbsd-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-netbsd-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-netbsd-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-netbsd-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-netbsd-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-netbsd-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-openbsd-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-openbsd-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-openbsd-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-openbsd-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-openbsd-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-openbsd-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-sunos-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-sunos-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-sunos-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-win32-arm64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-win32-arm64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-win32-arm64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm @typescript/typescript-win32-x64 under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: pnpm-lock.yaml → npm/typescript@7.0.2 → npm/@typescript/typescript-win32-x64@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript/typescript-win32-x64@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
License policy violation: npm typescript under unrecognized license

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/NOTICE.txt)

From: package.json → npm/typescript@7.0.2

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/typescript@7.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn

View full report

footer 0.3 renders a newsletter form with its own email input on every page,
so a page-wide count found two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 06f33f5 into master Oct 26, 2026
4 checks passed
@ralyodio
ralyodio deleted the chore/deps-2026-10-26 branch October 26, 2026 14:36
sh1pt-actions-fleet Bot added a commit that referenced this pull request Oct 29, 2026
* fix(jobs): write only the fields a listing patch sent

* fix(jobs): write only the fields a listing patch sent

* fix(jobs): write only the fields a listing patch sent

* fix(watches): handle concurrent duplicate saves (#212)

* Make bulk operator assignments atomic (#213)

* Test bulk operator assignment atomicity

* Make bulk operator assignments atomic

* fix(inbox): open long threads at recent messages (#217)

* fix(tracker): reject invalid calendar dates (#218)

* fix(recommendations): keep rewrite signature selected (#219)

* fix(resumes): cap collision public slugs at 60 characters (#220)

* fix(jobs): return numeric search total from bigint count (#221)

* fix(import): find ZIP directory past signature-like comments (#222)

* fix(pay): reject invalid supplied amounts (#223)

* fix(updates): serialize concurrent author posts (#224)

* fix(agents): detect operator cycles across full account depth (#225)

* fix(browse): keep semantic resume headers (#226)

* fix(resume): parse angle-bracketed contact links (#227)

* fix(push): fit notifications within service size limit (#228)

* fix(rankings): include older reads in all-time popular board (#229)

* fix(import): preserve comparison text in job descriptions (#230)

* fix(capacity): alert on older incomplete public resumes (#231)

* fix(openprofile): preserve mailto operator contact details (#232)

* fix(post): include employer when drafting a job (#233)

* fix(notifications): keep displayed read set complete (#234)

* fix(feed): preserve filtered JSON feed URL (#235)

* fix(mcp): forward plural search tags filter (#236)

* fix(applications): save candidate metadata atomically (#237)

* fix(inbox): reject ambiguous message targets (#238)

* fix(directory): count each topic instance once (#239)

* fix(feed): ignore view controls when filtering site RSS (#240)

* fix(resumes): retry public slug collisions (#241)

* fix(import): preserve HTML email contact targets (#243)

* fix(mcp): report failed notification mark-read (#244)

* fix(directory): treat search wildcards literally (#242)

* fix(applications): preserve prototype-named answers (#245)

* fix(agents): reject invalid profile URLs (#246)

* fix(resumes): strip numbered skill list markers (#247)

* fix(agents): enforce registration cap atomically (#248)

* fix(applications): paginate employer inbox (#249)

* fix(openprofile): export plus-bullet accounts (#250)

* fix(agents): serialize operator graph updates (#251)

* fix(applications): preserve select option labels (#252)

* fix(directory): strip emphasis from resume names (#253)

* fix(resumes): invalidate stale uploaded downloads (#254)

* fix(apply): show agent disclosure validation error (#255)

* fix(tui): include draft jobs in own listings (#256)

* fix(candidates): avoid repeating resume header (#257)

* fix(apply): keep drafts from sending to closed jobs (#258)

* fix(directory): redact phone numbers from profile summaries (#259)

* fix(cli): return updated notifications after marking read (#260)

* fix(jobs): preserve omitted settings in patches (#261)

* fix(import): detect mislabeled Word resumes (#262)

* fix(search): require every requested job tag (#263)

* fix(discovery): omit inactive jobs from sitemaps (#264)

* fix(inbox): reuse threads for concurrent first messages (#265)

* test: fix idempotency suite skip guard for empty Postgres error messages  A failed pg connection can produce an error with an empty .message, so reason === '' and the no-database skip guard never engages; the four subtests then crash with "Cannot read properties of null (reading 'query')". Track readiness with an explicit flag instead: 741 pass / 0 fail / 4 skipped without a database (was 741/4/0), and all 745 pass with one. (#268)

* Tracker: show zero and name what is missing instead of "Unknown" (#269)

A fleet with only cost imports showed "Unknown" on five of six cards, which
read as broken. The dashboard now shows 0 / USD 0 / 0.0% and says under each
card what is missing (no work imported, no receipts/commissions/fees/affiliate
payouts, cost entries with no amount). Incomplete profit shows zero with the
costs imported so far as a note, not a negative number made of estimates.

Adds a "How to improve" list: apply to more jobs, post jobs, list on the
leaderboard, raise a rate below cost + target, bill hours that have costs,
invoice tracked hours, and close a gap to the retained target.

summarize() is unchanged, so the API and CLI still report null for no data.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.21.1 (#270)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* New logo, favicon and the w3bs.org look (#271)

The mark is a briefcase in orbit with an agent riding the ring: the same
orbit family as w3bs.org's, in this board's green with w3bs orange for the
agent. favicon.svg is a heavier cut of it that holds at 16px, and logo.svg
is the mark with a heavy "agenticjobs." wordmark.

PNGs are generated by the fav CLI: icon-192/512, apple-touch-icon,
favicon-16/32 and favicon.ico. The manifest had named icon-192.png and
icon-512.png while neither existed, so install prompts got a 404; /favicon.ico
and /apple-touch-icon.png now answer at the root too.

The site takes w3bs.org's paper-and-ink palette, light serif headlines with
an orange italic accent, and its wordmark style in the header, in both
themes. A test fails if a manifest icon ever 404s again.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.0 (#272)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1: job titles at the serif headline weight (#273)

The 0.22.0 serif headings left .job-title at weight 650, which smears in
Georgia at card size. Titles now sit at 400 and get size instead, like the
card titles.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1 (#274)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(resume): keep undated role subtitles out of date fields (#275)

* 0.22.2: header logo at 3rem (#276)

The mark goes from 2rem to 3rem, and the header's minimum height from
3.5rem to 4rem so a 48px mark is not pressed against its edges.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ops: deploy agenticjobs.work to dev2 on merge (#277)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice (#278)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice

pg copies whatever pg-connection-string parses out of the connection string
over the options passed to Pool, and since pg-connection-string 2.9 a plain
sslmode=require means full certificate verification. So the PGSSLMODE opt-in
here was dead code whenever DATABASE_URL carried sslmode, and a database with
a self-signed certificate (the shared cluster on dev2) refused every
connection with 'self-signed certificate'. Strip sslmode from the string and
let needsSsl() decide, as it was always meant to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: parse timestamptz offsets that carry seconds (LMT timezones)  A timestamptz older than the server timezone's adoption of standard time renders with the local-mean-time offset, which carries seconds - e.g. `1900-01-15 08:27:52+08:27:52` when Postgres runs in Asia/Seoul. Date() rejects an offset with seconds, the 1184 type parser's toISOString() then threw RangeError: Invalid time value, and every route reading such a row answered 500 (caught by the expired-jobs test on any contributor machine in an LMT timezone; CI passes only because postgres:17-alpine runs in UTC). Truncate the offset to whole minutes before parsing: under a second of drift, and only on dates far older than any job on the board. Suite 864/864 on a fresh Postgres 18.4 in Asia/Seoul (was 863/864 with the 500). (#279)

Updated timestamp parsing to handle local mean time offsets and added a new function for parsing timestamps.

* updates: refuse a link to a board page that does not exist (#280)

A reader reported that the 2026-09-13 update "Matt: Open for small research
work" links to /candidates/matt-find-it-research-ilands-agent, which 404s,
and that /updates and /feed.rss ship it. The author's page is /candidates/matt:
the posting agent wrote the address it expected rather than the one the board
minted, and the link column accepted any public URL without looking.

A link back into this board (same host, www or not) to /candidates/<slug>,
/jobs/<slug> or /employers/<slug> now has to name a page that is served.
Otherwise posting fails with a sentence saying so, and a candidate is told
their own page. Links off the board are unchanged.

Migration 0023 repairs the one stored row, keyed on its id and the exact dead
value so it is a no-op everywhere else.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Reject array values in structured pay amounts (#281)

* Reject array values in structured pay amounts

* Cover array pay amounts and valid numeric inputs

* fix(watches): serialize account limit checks (#282)

Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>

* Run the board on Bun in the image (#283)

* Run the board on Bun in the image

The runtime stage moves from node:24-slim to Debian 12 + the Bun 1.4.0
binary. The pnpm/Node build stage, the lockfile and the npm package's
Node 24 contract are unchanged; only the process that runs dist/ changes.
Debian 12 keeps pandoc, weasyprint and poppler at the versions resumes
were converted with before.

- serve exits on SIGTERM/SIGINT, so docker stop no longer waits 10 s
- HEALTHCHECK on /api/v1/stats
- CI runs the suite under bun test as well and checks the image runs bun
- railway.json start command follows the image

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: print a pass/fail tally for the Bun test run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.3 (#284)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: Profullstack webring links in the footer (#287)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.4 (#288)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix uppercase pay-period words being parsed as currencies (#289)

* Fix uppercase pay-period suffix parsing

* Add uppercase pay-period regression tests

* fix(sync): persist metadata filled into existing boards (#290)

* fix(client): preserve the fanout timeout budget (#291)

* feat(client): allow shared-deadline callers to disable timeout retries

* fix(client): do not retry an exhausted fanout deadline

* test(client): cover exhausted fanout retries and healthy peers

* Add GET /api/health for the status page (#292)

Returns {status:ok, db:ok} with Cache-Control: no-store after a select 1
against the pool, or 503 {status:error, db:down} if the database does not
answer within 3s. No error details leave the process.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(client): preserve defaults for optional network search filters (#293)

* fix(client): do not retry writes protected only by a blank key (#294)

* fix(markdown): split lists when marker style changes (#295)

Co-authored-by: xiaoliang <xiaoliang@tinypace.com>

* fix(cli): preserve candidate slug in news filters (#296)

* fix(cli): preserve candidate slug in news filters

* test(cli): cover candidate news filters and subject switches

* fix(search): deduplicate tags before applying query cap (#298)

* fix(cli): strip closing Markdown hashes from derived job titles (#299)

* fix(cli): accept stdin dash as an option value (rebased #297) (#300)

* fix(cli): accept stdin dash as an option value

* test(cli): cover piped tracker imports and dash parsing

---------

Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>

* fix: withhold contact URIs from anonymous resume views (#302)

* fix: withhold explicit contact URIs from anonymous resumes

Added functions to redact contact URIs and check for contact channels in text.

* fix: guard cached candidate location and skills contact channels

Refactor skill extraction logic to improve handling of category labels and contact channels.

* test: cover encoded contact URIs and anonymous candidate summaries

* test(client): give the fanout timeout test a budget CI can meet (#303)

The healthy mock board shares the 50 ms budget with the slow one, so on a
loaded runner it timed out too and line 32 failed. It caused 3 of the last 4
master CI failures and blocked #301. 1 s keeps the assertion (one slow call,
no budget restart) while leaving the healthy board ample headroom.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): infer job titles from indented Markdown headings (#301)

* fix(cli): recognize indented Markdown job titles

* test(cli): cover indented title extraction and code boundaries

* fix(cli): preserve leading code indentation after front matter

Updated body extraction to remove blank opening lines while preserving indentation for Markdown syntax.

* test(cli): retain initial code examples during title extraction

* fix(mcp): suppress responses to notifications (#304)

* fix(markdown): preserve literal link destinations (#305)

* fix(resume): retain indented headings in candidate discovery (#306)

* feat(webring): Random link in the Profullstack ring footer (#307)

Also serves /.well-known/openwebring.json (agenticjobs.work is member
agenticjobs-work-3 in the ring), which the board did not have.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(footer): use @profullstack/footer (#309)

The board's link row and the Profullstack ring nav move into the shared footer
package, rendered on the server from its @latest template (kept and refreshed
in the background, since Layout renders synchronously). The popular-skills row
stays as the site's own footer above it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(markdown): preserve blank-separated list siblings (#308)

* fix(markdown): strip complete link destinations from summaries (#310)

* fix(footer): drop the skills row's own rule above the shared footer (#311)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(query): drop NUL bytes in string filters instead of answering 500 (#312)

PostgreSQL refuses a NUL byte in text outright, so a single `%00` in any
string filter reached the LIKE pattern and turned the request into a 500:

- `GET /api/v1/jobs?q=%00`, `?org=%00`, `?tags=%00` - and every surface
  that shares parseQuery with it: the page, MCP, the CLI and federated
  search at once.
- `GET /api/v1/directory/instances?q=%00` and `?topic=%00`.

Both filters now pass through the same clean() gate the rest of the board
uses, so the byte is dropped the way an unknown value is dropped rather
than turned into an error, and a filter that cleans away to nothing is
treated as absent rather than as a search for the empty string.

Verified against the live board before the change: `?q=%00` answered 500
on both routes while `%01` and `%1f` answered 200, which is what pointed
at the NUL byte specifically rather than at control characters in general.

Co-authored-by: muazaulia <ojikaminari@gmail.com>

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard the decode that throws (#313)

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard decode

The rest of the class #312 started. PostgreSQL refuses a NUL byte in a
text parameter outright, so every lookup that took a slug or an id
straight from a request answered 500 where an unknown value answers
"nothing here": "org" on POST /jobs and /updates, "job", "as",
"candidate" and "employer" on POST /inbox, "resumeId" and "resumeSlug"
on POST /jobs/:slug/apply, ?candidate=%00 on /inbox/new, ?state=%00 on
the CoinPay callback, and a board link whose path carries %00 - or a
malformed escape, which decodeURIComponent throws on, uncaught until
now.

Drop the byte at each boundary, the way parseQuery drops it from a
search, and treat a value that cleans away to nothing as absent rather
than as a lookup for the empty string. One test per boundary.

* fix(pages): read the logout cookie the way the middleware does

The route parsed the cookie header by hand and ran decodeURIComponent
on the value, so `Cookie: aj_session=%E0%A4%A` - a malformed escape -
threw a URIError out of the route and answered 500 where every token
that matches no session answers the redirect home. Read it with
getCookie, the same helper the middleware uses, which leaves what it
cannot decode alone.

Verified live before the change: the request answered 500. Three tests
against a real Postgres: the malformed escape, a NUL byte, and a real
session still being revoked.

* fix(resume): preserve contact links with trailing destination whitespace (#314)

* fix(resume): accept whitespace before a link closing parenthesis

Preserve contact destinations when valid Markdown links include trailing spaces or tabs. AI-assisted change, reproduced and tested locally.

* Create resume-link-space.test.ts

* fix(docs): OpenCreds and ASDLC links point at logicsrc.com (#315)

/docs/openprofile was copied from logicsrc.com, where /opencreds and /asdlc
are local pages. On agenticjobs.work they 404ed (ThreatCrush Link Watch).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Add a CrawlProof ad unit above the footer on public pages (#316)

One text_link unit served as a cross-origin /api/ads/frame iframe for slot
9c3d6e3d-ae1f-43ff-b6d9-4f42f11725ce. The board's strict CSP gains only
frame-src https://crawlproof.com; no ad.js, no extra script or storage.
Skipped on noindex pages and on flows (tracker, post, me, inbox, auth...).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.5: draft writer falls back to OpenAI, and says when an account is empty (#317)

The /post drafter only ever called one provider, and reported OpenAI's
"no credits remaining" 429 as "rate limited, try again shortly". Now
Anthropic is tried first and OpenAI answers when it fails; an exhausted
balance on either says "out of credits"; the provider's raw answer is
logged server-side (never shown); WRITER_MODEL only goes to the
provider it names.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.6: pit.world global footer; the site footer is two rows (#318)

- src/views/pit-footer.ts replaces pfs-footer.ts: the snippet from
  https://pit.world/snippets/footer.html?site=<board host>, a per-host module
  cache refreshed hourly in the background (3s timeout, last good copy kept,
  nothing rendered if never fetched), last in <body>.
- Footer: the popular-skills row stays; the @profullstack/footer bar (nine
  links, tagline, webring) becomes one row: copyright, Popular, Agents, API,
  Feed, Source. The ring now comes from the global footer.
- The CrawlProof unit moves from above the footer to the top of <main>.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.7: restore the @profullstack/footer bar under the site footer (#319)

The webring bar is being rolled out fleet-wide on purpose. It comes back with
no extra links (the site footer row carries them), between the site footer and
the pit.world global footer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: exempt all @profullstack/* from minimumReleaseAge (#320)

Our own packages; version-pinned excludes made every fleet bump wait 24h.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* No ads on the homepage; low-profile text-link ads elsewhere (#321)

* No ads on the homepage; low-profile text-link ads elsewhere

Homepage renders no ad unit. Remaining units are a single text_link
line per page with quiet spacing, never against the footer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump the service worker cache VERSION for the app.css change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(invoices): send payer, payee and property to CoinPay; read the nested webhook (#322)

requestPayment now passes customer_email (payer email), account_id
(payer id) and property agenticjobs.work top-level on
/api/payments/create, and the same plus payee_id in metadata. The
peer-invoice model is unchanged: the payer still pays the payee's own
wallet via merchant_wallet_address.

applyWebhook read event/payment_id flat, so a real nested CoinPay
webhook ({id: evt_..., type, data: {payment_id, status}}) was always
ignored and invoices only settled on the next page load. It now reads
data first with the flat shape as a fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(crawl): sell training crawlers a day pass on /crawl (x402-gateway 0.11) (#323)

The crawl gateway runs first when COINPAY_X402_KEY and CRAWL_PAY_TO are
set: GPTBot, ClaudeBot, headless and SEO scrapers get a 402 with
PAYMENT-REQUIRED and the /crawl sales page (x402 USDC or the CoinPay Buy
a day pass checkout) before the board spends a query on them. People,
verified search crawlers, AI chat/search fetchers and aj_session holders
pass through; /api/health stays open. A board without the two settings
never sells.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(profile): shared signup profile fields via @profullstack/profile (#324)

/signup asks for first_name, last_name, company_domain, job_title and
linkedin_url beside the email box (plain /login never does). The fields are
validated before any link is made, ride on the magic link (magic_links.profile)
because the account is created when the link is spent, and are saved into the
package's user_profiles table on /auth/callback. A failed save never blocks
sign-in.

GET/PUT/PATCH /api/v1/me/profile are the package's handlers; /me gets a
private Contact details form. Migration 0024 creates user_profiles and
backfills first/last name from users.name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): upgrade all dependencies (0.22.8) (#325)

* chore(deps): upgrade all dependencies (0.22.8)

@hono/node-server 2, typescript 7, hqtui 0.11.1, x402-gateway 0.13.1, profile
0.1.6, synconfig 0.1.3, hono 4.13.14, pg 8.23.1, simplewebauthn 14.0.3. Bun
runtime image 1.4.4. Boot-tested with a throwaway Postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(signup): count email boxes inside the signup form only

footer 0.3 renders a newsletter form with its own email input on every page,
so a page-wide count found two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(credits): credit codes, invites and ?ref= links (0.23.0) (#326)

A code (AJ-XXXX-XXXX-XXXX-XXXX, stored only as a hash) grants credit in US
cents, redeemed on /me, over the API, the CLI or MCP
(@profullstack/redeem-credits). Every account gets three single-use /i/<code>
invites at /invites plus one open ?ref= link (@profullstack/affiliates).

- The invite and ref ride on the magic link, so a link opened in another
  browser still counts, and they apply only when that link makes a new account
  (the address is confirmed by construction).
- An accepted invite pays both sides INVITE_BONUS_CENTS (default 100) unless the
  two addresses are one mailbox, the inviter's account is younger than
  INVITE_MIN_INVITER_AGE_HOURS (default 24), or the inviter has had ten bonuses
  in 30 days. invite_bonuses claims the row before the grant, so a retry never
  pays twice.
- /i/ is open past the crawl gateway; links are built from PUBLIC_URL, or the
  request host when that is still localhost.
- Operator: agenticjobs credits mint|list|revoke against DATABASE_URL; admin
  POST /api/v1/admin/credit-codes from a browser session.
- The board charges no listing fee or commission, so nothing spends credit yet.
- api.test: retry an agent path once when a parallel test hid the agent.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.1, never charge sign-in or app paths (#327)

Since gateway 0.8 a stale or headless Chrome string was answered 402 on
/login, /signup, OAuth callbacks and /api/auth; 0.15 never charges those.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.2, old browsers get the script challenge (#328)

A desktop Chrome two years behind was answered 402 on every content page;
0.15.2 gives it the script challenge first, so a person on an old office
machine reads.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(coinpay): keep payees' wallet addresses, never their CoinPay tokens (0.23.1) (#329)

coinpay_accounts held every connected person's CoinPay access and refresh
token in plaintext, readable by anyone with the database or root on dev2.
The board only ever used them for one call, /oauth/userinfo, to re-read the
person's receiving wallets on "Refresh wallets". Payments are minted with the
board's own business key, never a user token. Addresses are not secrets, so
keep those and stop storing the tokens.

- The OAuth callback writes the wallets (+ wallets_synced_at) and nulls the
  token columns in the same upsert. "Refresh wallets" is now a fresh consent
  (POST /me/coinpay/refresh redirects to /me/coinpay/connect).
- usable = the grant had wallet:read; no token is needed to invoice.
- Migration 0026: token columns nullable, wallets_synced_at added (additive).
- Legacy rows are converted by a sweep at boot (then every 6h until none are
  left) and by `agenticjobs coinpay-sweep`: one refresh + userinfo, wallets
  written and tokens nulled in one update. A token CoinPay refuses is deleted
  but the wallets read at connect time stay usable. An outage changes nothing.
- LEGACY_KEY_MIGRATION=off never sends a legacy token anywhere again; the
  sweep only deletes them. The sweep reports `remaining`.

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.15.3 (browsers asking for a page are never 402'd) (#330)

Chrome 120-132 and Firefox 130 desktop user agents got the x402 JSON offer
instead of the page; 0.15.3 lets any request that asks like a browser read
it. Declared AI crawlers are still charged. profullstack/x402-gateway#28

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.16.0 (one fetch reads the page) (#331)

A stale, spoofed or headless browser string now reads 20 pages an hour per
address before the 402, so a person or an agent fetching one page gets it
and a bulk crawl still pays. Declared AI crawlers are still charged on
sight. New authenticate hook for sites with API keys.
profullstack/x402-gateway#29

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(apply): reject a malformed resume id before it reaches Postgres (#267)

* fix(apply): reject a malformed resume id before it reaches Postgres

* fix(apply): reject a malformed resume id before it reaches Postgres

---------

Co-authored-by: RowletCC <chengjacky073@gmail.com>
Co-authored-by: kepler-ops-maker <sungmin@mail.instinct.com>
Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: SII-Zhuangqiu Huang <46834192+hzhuangqiu@users.noreply.github.com>
Co-authored-by: zhonglingzlzl-lgtm <zhonglingzlzl@gmail.com>
Co-authored-by: waytzhang <chao.zhang@crimsoneducation.org>
Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>
Co-authored-by: k1mtruth <56712246+k1mtruth@users.noreply.github.com>
Co-authored-by: zelumi <sun2000feng@gmail.com>
Co-authored-by: ilanvkn <ilan.vkn@gmail.com>
Co-authored-by: bmdy5 <110339793+bmdy5@users.noreply.github.com>
Co-authored-by: xiaoliang <xiaoliang@tinypace.com>
Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>
Co-authored-by: Ayush Agrawal <131985798+ayush-agrawal001@users.noreply.github.com>
Co-authored-by: reacx-research <reacx@proton.me>
Co-authored-by: Dayire <s.rachid.riad@gmail.com>
Co-authored-by: ouji kaminari <kaminariouji@gmail.com>
Co-authored-by: muazaulia <ojikaminari@gmail.com>
Co-authored-by: shkim3142-ops <shkim3142@gmail.com>
Co-authored-by: sh1pt-actions-fleet[bot] <287014002+sh1pt-actions-fleet[bot]@users.noreply.github.com>
sh1pt-actions-fleet Bot added a commit that referenced this pull request Oct 29, 2026
)

* fix(markup): keep autolink and emphasis out of emitted link markup

* fix(markup): keep autolink and emphasis out of emitted link markup

* test: locate website binding after partial org updates (#215)

* fix(watches): handle concurrent duplicate saves (#212)

* Make bulk operator assignments atomic (#213)

* Test bulk operator assignment atomicity

* Make bulk operator assignments atomic

* fix(inbox): open long threads at recent messages (#217)

* fix(tracker): reject invalid calendar dates (#218)

* fix(recommendations): keep rewrite signature selected (#219)

* fix(resumes): cap collision public slugs at 60 characters (#220)

* fix(jobs): return numeric search total from bigint count (#221)

* fix(import): find ZIP directory past signature-like comments (#222)

* fix(pay): reject invalid supplied amounts (#223)

* fix(updates): serialize concurrent author posts (#224)

* fix(agents): detect operator cycles across full account depth (#225)

* fix(browse): keep semantic resume headers (#226)

* fix(resume): parse angle-bracketed contact links (#227)

* fix(push): fit notifications within service size limit (#228)

* fix(rankings): include older reads in all-time popular board (#229)

* fix(import): preserve comparison text in job descriptions (#230)

* fix(capacity): alert on older incomplete public resumes (#231)

* fix(openprofile): preserve mailto operator contact details (#232)

* fix(post): include employer when drafting a job (#233)

* fix(notifications): keep displayed read set complete (#234)

* fix(feed): preserve filtered JSON feed URL (#235)

* fix(mcp): forward plural search tags filter (#236)

* fix(applications): save candidate metadata atomically (#237)

* fix(inbox): reject ambiguous message targets (#238)

* fix(directory): count each topic instance once (#239)

* fix(feed): ignore view controls when filtering site RSS (#240)

* fix(resumes): retry public slug collisions (#241)

* fix(import): preserve HTML email contact targets (#243)

* fix(mcp): report failed notification mark-read (#244)

* fix(directory): treat search wildcards literally (#242)

* fix(applications): preserve prototype-named answers (#245)

* fix(agents): reject invalid profile URLs (#246)

* fix(resumes): strip numbered skill list markers (#247)

* fix(agents): enforce registration cap atomically (#248)

* fix(applications): paginate employer inbox (#249)

* fix(openprofile): export plus-bullet accounts (#250)

* fix(agents): serialize operator graph updates (#251)

* fix(applications): preserve select option labels (#252)

* fix(directory): strip emphasis from resume names (#253)

* fix(resumes): invalidate stale uploaded downloads (#254)

* fix(apply): show agent disclosure validation error (#255)

* fix(tui): include draft jobs in own listings (#256)

* fix(candidates): avoid repeating resume header (#257)

* fix(apply): keep drafts from sending to closed jobs (#258)

* fix(directory): redact phone numbers from profile summaries (#259)

* fix(cli): return updated notifications after marking read (#260)

* fix(jobs): preserve omitted settings in patches (#261)

* fix(import): detect mislabeled Word resumes (#262)

* fix(search): require every requested job tag (#263)

* fix(discovery): omit inactive jobs from sitemaps (#264)

* fix(inbox): reuse threads for concurrent first messages (#265)

* test: fix idempotency suite skip guard for empty Postgres error messages  A failed pg connection can produce an error with an empty .message, so reason === '' and the no-database skip guard never engages; the four subtests then crash with "Cannot read properties of null (reading 'query')". Track readiness with an explicit flag instead: 741 pass / 0 fail / 4 skipped without a database (was 741/4/0), and all 745 pass with one. (#268)

* Tracker: show zero and name what is missing instead of "Unknown" (#269)

A fleet with only cost imports showed "Unknown" on five of six cards, which
read as broken. The dashboard now shows 0 / USD 0 / 0.0% and says under each
card what is missing (no work imported, no receipts/commissions/fees/affiliate
payouts, cost entries with no amount). Incomplete profit shows zero with the
costs imported so far as a note, not a negative number made of estimates.

Adds a "How to improve" list: apply to more jobs, post jobs, list on the
leaderboard, raise a rate below cost + target, bill hours that have costs,
invoice tracked hours, and close a gap to the retained target.

summarize() is unchanged, so the API and CLI still report null for no data.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.21.1 (#270)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* New logo, favicon and the w3bs.org look (#271)

The mark is a briefcase in orbit with an agent riding the ring: the same
orbit family as w3bs.org's, in this board's green with w3bs orange for the
agent. favicon.svg is a heavier cut of it that holds at 16px, and logo.svg
is the mark with a heavy "agenticjobs." wordmark.

PNGs are generated by the fav CLI: icon-192/512, apple-touch-icon,
favicon-16/32 and favicon.ico. The manifest had named icon-192.png and
icon-512.png while neither existed, so install prompts got a 404; /favicon.ico
and /apple-touch-icon.png now answer at the root too.

The site takes w3bs.org's paper-and-ink palette, light serif headlines with
an orange italic accent, and its wordmark style in the header, in both
themes. A test fails if a manifest icon ever 404s again.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.0 (#272)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1: job titles at the serif headline weight (#273)

The 0.22.0 serif headings left .job-title at weight 650, which smears in
Georgia at card size. Titles now sit at 400 and get size instead, like the
card titles.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1 (#274)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(resume): keep undated role subtitles out of date fields (#275)

* 0.22.2: header logo at 3rem (#276)

The mark goes from 2rem to 3rem, and the header's minimum height from
3.5rem to 4rem so a 48px mark is not pressed against its edges.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ops: deploy agenticjobs.work to dev2 on merge (#277)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice (#278)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice

pg copies whatever pg-connection-string parses out of the connection string
over the options passed to Pool, and since pg-connection-string 2.9 a plain
sslmode=require means full certificate verification. So the PGSSLMODE opt-in
here was dead code whenever DATABASE_URL carried sslmode, and a database with
a self-signed certificate (the shared cluster on dev2) refused every
connection with 'self-signed certificate'. Strip sslmode from the string and
let needsSsl() decide, as it was always meant to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: parse timestamptz offsets that carry seconds (LMT timezones)  A timestamptz older than the server timezone's adoption of standard time renders with the local-mean-time offset, which carries seconds - e.g. `1900-01-15 08:27:52+08:27:52` when Postgres runs in Asia/Seoul. Date() rejects an offset with seconds, the 1184 type parser's toISOString() then threw RangeError: Invalid time value, and every route reading such a row answered 500 (caught by the expired-jobs test on any contributor machine in an LMT timezone; CI passes only because postgres:17-alpine runs in UTC). Truncate the offset to whole minutes before parsing: under a second of drift, and only on dates far older than any job on the board. Suite 864/864 on a fresh Postgres 18.4 in Asia/Seoul (was 863/864 with the 500). (#279)

Updated timestamp parsing to handle local mean time offsets and added a new function for parsing timestamps.

* updates: refuse a link to a board page that does not exist (#280)

A reader reported that the 2026-09-13 update "Matt: Open for small research
work" links to /candidates/matt-find-it-research-ilands-agent, which 404s,
and that /updates and /feed.rss ship it. The author's page is /candidates/matt:
the posting agent wrote the address it expected rather than the one the board
minted, and the link column accepted any public URL without looking.

A link back into this board (same host, www or not) to /candidates/<slug>,
/jobs/<slug> or /employers/<slug> now has to name a page that is served.
Otherwise posting fails with a sentence saying so, and a candidate is told
their own page. Links off the board are unchanged.

Migration 0023 repairs the one stored row, keyed on its id and the exact dead
value so it is a no-op everywhere else.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Reject array values in structured pay amounts (#281)

* Reject array values in structured pay amounts

* Cover array pay amounts and valid numeric inputs

* fix(watches): serialize account limit checks (#282)

Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>

* Run the board on Bun in the image (#283)

* Run the board on Bun in the image

The runtime stage moves from node:24-slim to Debian 12 + the Bun 1.4.0
binary. The pnpm/Node build stage, the lockfile and the npm package's
Node 24 contract are unchanged; only the process that runs dist/ changes.
Debian 12 keeps pandoc, weasyprint and poppler at the versions resumes
were converted with before.

- serve exits on SIGTERM/SIGINT, so docker stop no longer waits 10 s
- HEALTHCHECK on /api/v1/stats
- CI runs the suite under bun test as well and checks the image runs bun
- railway.json start command follows the image

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: print a pass/fail tally for the Bun test run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.3 (#284)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: Profullstack webring links in the footer (#287)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.4 (#288)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix uppercase pay-period words being parsed as currencies (#289)

* Fix uppercase pay-period suffix parsing

* Add uppercase pay-period regression tests

* fix(sync): persist metadata filled into existing boards (#290)

* fix(client): preserve the fanout timeout budget (#291)

* feat(client): allow shared-deadline callers to disable timeout retries

* fix(client): do not retry an exhausted fanout deadline

* test(client): cover exhausted fanout retries and healthy peers

* Add GET /api/health for the status page (#292)

Returns {status:ok, db:ok} with Cache-Control: no-store after a select 1
against the pool, or 503 {status:error, db:down} if the database does not
answer within 3s. No error details leave the process.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(client): preserve defaults for optional network search filters (#293)

* fix(client): do not retry writes protected only by a blank key (#294)

* fix(markdown): split lists when marker style changes (#295)

Co-authored-by: xiaoliang <xiaoliang@tinypace.com>

* fix(cli): preserve candidate slug in news filters (#296)

* fix(cli): preserve candidate slug in news filters

* test(cli): cover candidate news filters and subject switches

* fix(search): deduplicate tags before applying query cap (#298)

* fix(cli): strip closing Markdown hashes from derived job titles (#299)

* fix(cli): accept stdin dash as an option value (rebased #297) (#300)

* fix(cli): accept stdin dash as an option value

* test(cli): cover piped tracker imports and dash parsing

---------

Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>

* fix: withhold contact URIs from anonymous resume views (#302)

* fix: withhold explicit contact URIs from anonymous resumes

Added functions to redact contact URIs and check for contact channels in text.

* fix: guard cached candidate location and skills contact channels

Refactor skill extraction logic to improve handling of category labels and contact channels.

* test: cover encoded contact URIs and anonymous candidate summaries

* test(client): give the fanout timeout test a budget CI can meet (#303)

The healthy mock board shares the 50 ms budget with the slow one, so on a
loaded runner it timed out too and line 32 failed. It caused 3 of the last 4
master CI failures and blocked #301. 1 s keeps the assertion (one slow call,
no budget restart) while leaving the healthy board ample headroom.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): infer job titles from indented Markdown headings (#301)

* fix(cli): recognize indented Markdown job titles

* test(cli): cover indented title extraction and code boundaries

* fix(cli): preserve leading code indentation after front matter

Updated body extraction to remove blank opening lines while preserving indentation for Markdown syntax.

* test(cli): retain initial code examples during title extraction

* fix(mcp): suppress responses to notifications (#304)

* fix(markdown): preserve literal link destinations (#305)

* fix(resume): retain indented headings in candidate discovery (#306)

* feat(webring): Random link in the Profullstack ring footer (#307)

Also serves /.well-known/openwebring.json (agenticjobs.work is member
agenticjobs-work-3 in the ring), which the board did not have.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(footer): use @profullstack/footer (#309)

The board's link row and the Profullstack ring nav move into the shared footer
package, rendered on the server from its @latest template (kept and refreshed
in the background, since Layout renders synchronously). The popular-skills row
stays as the site's own footer above it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(markdown): preserve blank-separated list siblings (#308)

* fix(markdown): strip complete link destinations from summaries (#310)

* fix(footer): drop the skills row's own rule above the shared footer (#311)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(query): drop NUL bytes in string filters instead of answering 500 (#312)

PostgreSQL refuses a NUL byte in text outright, so a single `%00` in any
string filter reached the LIKE pattern and turned the request into a 500:

- `GET /api/v1/jobs?q=%00`, `?org=%00`, `?tags=%00` - and every surface
  that shares parseQuery with it: the page, MCP, the CLI and federated
  search at once.
- `GET /api/v1/directory/instances?q=%00` and `?topic=%00`.

Both filters now pass through the same clean() gate the rest of the board
uses, so the byte is dropped the way an unknown value is dropped rather
than turned into an error, and a filter that cleans away to nothing is
treated as absent rather than as a search for the empty string.

Verified against the live board before the change: `?q=%00` answered 500
on both routes while `%01` and `%1f` answered 200, which is what pointed
at the NUL byte specifically rather than at control characters in general.

Co-authored-by: muazaulia <ojikaminari@gmail.com>

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard the decode that throws (#313)

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard decode

The rest of the class #312 started. PostgreSQL refuses a NUL byte in a
text parameter outright, so every lookup that took a slug or an id
straight from a request answered 500 where an unknown value answers
"nothing here": "org" on POST /jobs and /updates, "job", "as",
"candidate" and "employer" on POST /inbox, "resumeId" and "resumeSlug"
on POST /jobs/:slug/apply, ?candidate=%00 on /inbox/new, ?state=%00 on
the CoinPay callback, and a board link whose path carries %00 - or a
malformed escape, which decodeURIComponent throws on, uncaught until
now.

Drop the byte at each boundary, the way parseQuery drops it from a
search, and treat a value that cleans away to nothing as absent rather
than as a lookup for the empty string. One test per boundary.

* fix(pages): read the logout cookie the way the middleware does

The route parsed the cookie header by hand and ran decodeURIComponent
on the value, so `Cookie: aj_session=%E0%A4%A` - a malformed escape -
threw a URIError out of the route and answered 500 where every token
that matches no session answers the redirect home. Read it with
getCookie, the same helper the middleware uses, which leaves what it
cannot decode alone.

Verified live before the change: the request answered 500. Three tests
against a real Postgres: the malformed escape, a NUL byte, and a real
session still being revoked.

* fix(resume): preserve contact links with trailing destination whitespace (#314)

* fix(resume): accept whitespace before a link closing parenthesis

Preserve contact destinations when valid Markdown links include trailing spaces or tabs. AI-assisted change, reproduced and tested locally.

* Create resume-link-space.test.ts

* fix(docs): OpenCreds and ASDLC links point at logicsrc.com (#315)

/docs/openprofile was copied from logicsrc.com, where /opencreds and /asdlc
are local pages. On agenticjobs.work they 404ed (ThreatCrush Link Watch).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Add a CrawlProof ad unit above the footer on public pages (#316)

One text_link unit served as a cross-origin /api/ads/frame iframe for slot
9c3d6e3d-ae1f-43ff-b6d9-4f42f11725ce. The board's strict CSP gains only
frame-src https://crawlproof.com; no ad.js, no extra script or storage.
Skipped on noindex pages and on flows (tracker, post, me, inbox, auth...).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.5: draft writer falls back to OpenAI, and says when an account is empty (#317)

The /post drafter only ever called one provider, and reported OpenAI's
"no credits remaining" 429 as "rate limited, try again shortly". Now
Anthropic is tried first and OpenAI answers when it fails; an exhausted
balance on either says "out of credits"; the provider's raw answer is
logged server-side (never shown); WRITER_MODEL only goes to the
provider it names.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.6: pit.world global footer; the site footer is two rows (#318)

- src/views/pit-footer.ts replaces pfs-footer.ts: the snippet from
  https://pit.world/snippets/footer.html?site=<board host>, a per-host module
  cache refreshed hourly in the background (3s timeout, last good copy kept,
  nothing rendered if never fetched), last in <body>.
- Footer: the popular-skills row stays; the @profullstack/footer bar (nine
  links, tagline, webring) becomes one row: copyright, Popular, Agents, API,
  Feed, Source. The ring now comes from the global footer.
- The CrawlProof unit moves from above the footer to the top of <main>.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.7: restore the @profullstack/footer bar under the site footer (#319)

The webring bar is being rolled out fleet-wide on purpose. It comes back with
no extra links (the site footer row carries them), between the site footer and
the pit.world global footer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: exempt all @profullstack/* from minimumReleaseAge (#320)

Our own packages; version-pinned excludes made every fleet bump wait 24h.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* No ads on the homepage; low-profile text-link ads elsewhere (#321)

* No ads on the homepage; low-profile text-link ads elsewhere

Homepage renders no ad unit. Remaining units are a single text_link
line per page with quiet spacing, never against the footer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump the service worker cache VERSION for the app.css change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(invoices): send payer, payee and property to CoinPay; read the nested webhook (#322)

requestPayment now passes customer_email (payer email), account_id
(payer id) and property agenticjobs.work top-level on
/api/payments/create, and the same plus payee_id in metadata. The
peer-invoice model is unchanged: the payer still pays the payee's own
wallet via merchant_wallet_address.

applyWebhook read event/payment_id flat, so a real nested CoinPay
webhook ({id: evt_..., type, data: {payment_id, status}}) was always
ignored and invoices only settled on the next page load. It now reads
data first with the flat shape as a fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(crawl): sell training crawlers a day pass on /crawl (x402-gateway 0.11) (#323)

The crawl gateway runs first when COINPAY_X402_KEY and CRAWL_PAY_TO are
set: GPTBot, ClaudeBot, headless and SEO scrapers get a 402 with
PAYMENT-REQUIRED and the /crawl sales page (x402 USDC or the CoinPay Buy
a day pass checkout) before the board spends a query on them. People,
verified search crawlers, AI chat/search fetchers and aj_session holders
pass through; /api/health stays open. A board without the two settings
never sells.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(profile): shared signup profile fields via @profullstack/profile (#324)

/signup asks for first_name, last_name, company_domain, job_title and
linkedin_url beside the email box (plain /login never does). The fields are
validated before any link is made, ride on the magic link (magic_links.profile)
because the account is created when the link is spent, and are saved into the
package's user_profiles table on /auth/callback. A failed save never blocks
sign-in.

GET/PUT/PATCH /api/v1/me/profile are the package's handlers; /me gets a
private Contact details form. Migration 0024 creates user_profiles and
backfills first/last name from users.name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): upgrade all dependencies (0.22.8) (#325)

* chore(deps): upgrade all dependencies (0.22.8)

@hono/node-server 2, typescript 7, hqtui 0.11.1, x402-gateway 0.13.1, profile
0.1.6, synconfig 0.1.3, hono 4.13.14, pg 8.23.1, simplewebauthn 14.0.3. Bun
runtime image 1.4.4. Boot-tested with a throwaway Postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(signup): count email boxes inside the signup form only

footer 0.3 renders a newsletter form with its own email input on every page,
so a page-wide count found two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(credits): credit codes, invites and ?ref= links (0.23.0) (#326)

A code (AJ-XXXX-XXXX-XXXX-XXXX, stored only as a hash) grants credit in US
cents, redeemed on /me, over the API, the CLI or MCP
(@profullstack/redeem-credits). Every account gets three single-use /i/<code>
invites at /invites plus one open ?ref= link (@profullstack/affiliates).

- The invite and ref ride on the magic link, so a link opened in another
  browser still counts, and they apply only when that link makes a new account
  (the address is confirmed by construction).
- An accepted invite pays both sides INVITE_BONUS_CENTS (default 100) unless the
  two addresses are one mailbox, the inviter's account is younger than
  INVITE_MIN_INVITER_AGE_HOURS (default 24), or the inviter has had ten bonuses
  in 30 days. invite_bonuses claims the row before the grant, so a retry never
  pays twice.
- /i/ is open past the crawl gateway; links are built from PUBLIC_URL, or the
  request host when that is still localhost.
- Operator: agenticjobs credits mint|list|revoke against DATABASE_URL; admin
  POST /api/v1/admin/credit-codes from a browser session.
- The board charges no listing fee or commission, so nothing spends credit yet.
- api.test: retry an agent path once when a parallel test hid the agent.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.1, never charge sign-in or app paths (#327)

Since gateway 0.8 a stale or headless Chrome string was answered 402 on
/login, /signup, OAuth callbacks and /api/auth; 0.15 never charges those.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.2, old browsers get the script challenge (#328)

A desktop Chrome two years behind was answered 402 on every content page;
0.15.2 gives it the script challenge first, so a person on an old office
machine reads.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(coinpay): keep payees' wallet addresses, never their CoinPay tokens (0.23.1) (#329)

coinpay_accounts held every connected person's CoinPay access and refresh
token in plaintext, readable by anyone with the database or root on dev2.
The board only ever used them for one call, /oauth/userinfo, to re-read the
person's receiving wallets on "Refresh wallets". Payments are minted with the
board's own business key, never a user token. Addresses are not secrets, so
keep those and stop storing the tokens.

- The OAuth callback writes the wallets (+ wallets_synced_at) and nulls the
  token columns in the same upsert. "Refresh wallets" is now a fresh consent
  (POST /me/coinpay/refresh redirects to /me/coinpay/connect).
- usable = the grant had wallet:read; no token is needed to invoice.
- Migration 0026: token columns nullable, wallets_synced_at added (additive).
- Legacy rows are converted by a sweep at boot (then every 6h until none are
  left) and by `agenticjobs coinpay-sweep`: one refresh + userinfo, wallets
  written and tokens nulled in one update. A token CoinPay refuses is deleted
  but the wallets read at connect time stay usable. An outage changes nothing.
- LEGACY_KEY_MIGRATION=off never sends a legacy token anywhere again; the
  sweep only deletes them. The sweep reports `remaining`.

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.15.3 (browsers asking for a page are never 402'd) (#330)

Chrome 120-132 and Firefox 130 desktop user agents got the x402 JSON offer
instead of the page; 0.15.3 lets any request that asks like a browser read
it. Declared AI crawlers are still charged. profullstack/x402-gateway#28

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.16.0 (one fetch reads the page) (#331)

A stale, spoofed or headless browser string now reads 20 pages an hour per
address before the 402, so a person or an agent fetching one page gets it
and a bulk crawl still pays. Declared AI crawlers are still charged on
sight. New authenticate hook for sites with API keys.
profullstack/x402-gateway#29

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(apply): reject a malformed resume id before it reaches Postgres (#267)

* fix(apply): reject a malformed resume id before it reaches Postgres

* fix(apply): reject a malformed resume id before it reaches Postgres

* fix(jobs): write only the fields a listing patch sent (#216)

* fix(jobs): write only the fields a listing patch sent

* fix(jobs): write only the fields a listing patch sent

* fix(jobs): write only the fields a listing patch sent

* fix(watches): handle concurrent duplicate saves (#212)

* Make bulk operator assignments atomic (#213)

* Test bulk operator assignment atomicity

* Make bulk operator assignments atomic

* fix(inbox): open long threads at recent messages (#217)

* fix(tracker): reject invalid calendar dates (#218)

* fix(recommendations): keep rewrite signature selected (#219)

* fix(resumes): cap collision public slugs at 60 characters (#220)

* fix(jobs): return numeric search total from bigint count (#221)

* fix(import): find ZIP directory past signature-like comments (#222)

* fix(pay): reject invalid supplied amounts (#223)

* fix(updates): serialize concurrent author posts (#224)

* fix(agents): detect operator cycles across full account depth (#225)

* fix(browse): keep semantic resume headers (#226)

* fix(resume): parse angle-bracketed contact links (#227)

* fix(push): fit notifications within service size limit (#228)

* fix(rankings): include older reads in all-time popular board (#229)

* fix(import): preserve comparison text in job descriptions (#230)

* fix(capacity): alert on older incomplete public resumes (#231)

* fix(openprofile): preserve mailto operator contact details (#232)

* fix(post): include employer when drafting a job (#233)

* fix(notifications): keep displayed read set complete (#234)

* fix(feed): preserve filtered JSON feed URL (#235)

* fix(mcp): forward plural search tags filter (#236)

* fix(applications): save candidate metadata atomically (#237)

* fix(inbox): reject ambiguous message targets (#238)

* fix(directory): count each topic instance once (#239)

* fix(feed): ignore view controls when filtering site RSS (#240)

* fix(resumes): retry public slug collisions (#241)

* fix(import): preserve HTML email contact targets (#243)

* fix(mcp): report failed notification mark-read (#244)

* fix(directory): treat search wildcards literally (#242)

* fix(applications): preserve prototype-named answers (#245)

* fix(agents): reject invalid profile URLs (#246)

* fix(resumes): strip numbered skill list markers (#247)

* fix(agents): enforce registration cap atomically (#248)

* fix(applications): paginate employer inbox (#249)

* fix(openprofile): export plus-bullet accounts (#250)

* fix(agents): serialize operator graph updates (#251)

* fix(applications): preserve select option labels (#252)

* fix(directory): strip emphasis from resume names (#253)

* fix(resumes): invalidate stale uploaded downloads (#254)

* fix(apply): show agent disclosure validation error (#255)

* fix(tui): include draft jobs in own listings (#256)

* fix(candidates): avoid repeating resume header (#257)

* fix(apply): keep drafts from sending to closed jobs (#258)

* fix(directory): redact phone numbers from profile summaries (#259)

* fix(cli): return updated notifications after marking read (#260)

* fix(jobs): preserve omitted settings in patches (#261)

* fix(import): detect mislabeled Word resumes (#262)

* fix(search): require every requested job tag (#263)

* fix(discovery): omit inactive jobs from sitemaps (#264)

* fix(inbox): reuse threads for concurrent first messages (#265)

* test: fix idempotency suite skip guard for empty Postgres error messages  A failed pg connection can produce an error with an empty .message, so reason === '' and the no-database skip guard never engages; the four subtests then crash with "Cannot read properties of null (reading 'query')". Track readiness with an explicit flag instead: 741 pass / 0 fail / 4 skipped without a database (was 741/4/0), and all 745 pass with one. (#268)

* Tracker: show zero and name what is missing instead of "Unknown" (#269)

A fleet with only cost imports showed "Unknown" on five of six cards, which
read as broken. The dashboard now shows 0 / USD 0 / 0.0% and says under each
card what is missing (no work imported, no receipts/commissions/fees/affiliate
payouts, cost entries with no amount). Incomplete profit shows zero with the
costs imported so far as a note, not a negative number made of estimates.

Adds a "How to improve" list: apply to more jobs, post jobs, list on the
leaderboard, raise a rate below cost + target, bill hours that have costs,
invoice tracked hours, and close a gap to the retained target.

summarize() is unchanged, so the API and CLI still report null for no data.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.21.1 (#270)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* New logo, favicon and the w3bs.org look (#271)

The mark is a briefcase in orbit with an agent riding the ring: the same
orbit family as w3bs.org's, in this board's green with w3bs orange for the
agent. favicon.svg is a heavier cut of it that holds at 16px, and logo.svg
is the mark with a heavy "agenticjobs." wordmark.

PNGs are generated by the fav CLI: icon-192/512, apple-touch-icon,
favicon-16/32 and favicon.ico. The manifest had named icon-192.png and
icon-512.png while neither existed, so install prompts got a 404; /favicon.ico
and /apple-touch-icon.png now answer at the root too.

The site takes w3bs.org's paper-and-ink palette, light serif headlines with
an orange italic accent, and its wordmark style in the header, in both
themes. A test fails if a manifest icon ever 404s again.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.0 (#272)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1: job titles at the serif headline weight (#273)

The 0.22.0 serif headings left .job-title at weight 650, which smears in
Georgia at card size. Titles now sit at 400 and get size instead, like the
card titles.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.1 (#274)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(resume): keep undated role subtitles out of date fields (#275)

* 0.22.2: header logo at 3rem (#276)

The mark goes from 2rem to 3rem, and the header's minimum height from
3.5rem to 4rem so a 48px mark is not pressed against its edges.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ops: deploy agenticjobs.work to dev2 on merge (#277)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice (#278)

* ops: deploy agenticjobs.work to dev2 on merge

Railway deployed this on push; dev2 does not by itself. The workflow ssh's
to the box and runs /home/anthony/www/agenticjobs.work/deploy-app.sh, which builds
the image from this checkout and restarts the compose stack. Generated by
cli-tools dev2/dev2-site scaffold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Dockerfile: chown the runtime files to node so a umask-007 checkout still boots

COPY keeps the build context's modes. Cloned under umask 007 (as the deploy
account on dev2 does) every file is 0660 and root owns it inside the image, so
USER node cannot read /app/package.json and the process dies at start with
ERR_INVALID_PACKAGE_CONFIG. Railway's builder happened to give us 0644 files,
which is why this never showed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* db: keep the URL's sslmode from overriding the pool's own SSL choice

pg copies whatever pg-connection-string parses out of the connection string
over the options passed to Pool, and since pg-connection-string 2.9 a plain
sslmode=require means full certificate verification. So the PGSSLMODE opt-in
here was dead code whenever DATABASE_URL carried sslmode, and a database with
a self-signed certificate (the shared cluster on dev2) refused every
connection with 'self-signed certificate'. Strip sslmode from the string and
let needsSsl() decide, as it was always meant to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* db: parse timestamptz offsets that carry seconds (LMT timezones)  A timestamptz older than the server timezone's adoption of standard time renders with the local-mean-time offset, which carries seconds - e.g. `1900-01-15 08:27:52+08:27:52` when Postgres runs in Asia/Seoul. Date() rejects an offset with seconds, the 1184 type parser's toISOString() then threw RangeError: Invalid time value, and every route reading such a row answered 500 (caught by the expired-jobs test on any contributor machine in an LMT timezone; CI passes only because postgres:17-alpine runs in UTC). Truncate the offset to whole minutes before parsing: under a second of drift, and only on dates far older than any job on the board. Suite 864/864 on a fresh Postgres 18.4 in Asia/Seoul (was 863/864 with the 500). (#279)

Updated timestamp parsing to handle local mean time offsets and added a new function for parsing timestamps.

* updates: refuse a link to a board page that does not exist (#280)

A reader reported that the 2026-09-13 update "Matt: Open for small research
work" links to /candidates/matt-find-it-research-ilands-agent, which 404s,
and that /updates and /feed.rss ship it. The author's page is /candidates/matt:
the posting agent wrote the address it expected rather than the one the board
minted, and the link column accepted any public URL without looking.

A link back into this board (same host, www or not) to /candidates/<slug>,
/jobs/<slug> or /employers/<slug> now has to name a page that is served.
Otherwise posting fails with a sentence saying so, and a candidate is told
their own page. Links off the board are unchanged.

Migration 0023 repairs the one stored row, keyed on its id and the exact dead
value so it is a no-op everywhere else.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Reject array values in structured pay amounts (#281)

* Reject array values in structured pay amounts

* Cover array pay amounts and valid numeric inputs

* fix(watches): serialize account limit checks (#282)

Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>

* Run the board on Bun in the image (#283)

* Run the board on Bun in the image

The runtime stage moves from node:24-slim to Debian 12 + the Bun 1.4.0
binary. The pnpm/Node build stage, the lockfile and the npm package's
Node 24 contract are unchanged; only the process that runs dist/ changes.
Debian 12 keeps pandoc, weasyprint and poppler at the versions resumes
were converted with before.

- serve exits on SIGTERM/SIGINT, so docker stop no longer waits 10 s
- HEALTHCHECK on /api/v1/stats
- CI runs the suite under bun test as well and checks the image runs bun
- railway.json start command follows the image

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: print a pass/fail tally for the Bun test run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* 0.22.3 (#284)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: Profullstack webring links in the footer (#287)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.4 (#288)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix uppercase pay-period words being parsed as currencies (#289)

* Fix uppercase pay-period suffix parsing

* Add uppercase pay-period regression tests

* fix(sync): persist metadata filled into existing boards (#290)

* fix(client): preserve the fanout timeout budget (#291)

* feat(client): allow shared-deadline callers to disable timeout retries

* fix(client): do not retry an exhausted fanout deadline

* test(client): cover exhausted fanout retries and healthy peers

* Add GET /api/health for the status page (#292)

Returns {status:ok, db:ok} with Cache-Control: no-store after a select 1
against the pool, or 503 {status:error, db:down} if the database does not
answer within 3s. No error details leave the process.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(client): preserve defaults for optional network search filters (#293)

* fix(client): do not retry writes protected only by a blank key (#294)

* fix(markdown): split lists when marker style changes (#295)

Co-authored-by: xiaoliang <xiaoliang@tinypace.com>

* fix(cli): preserve candidate slug in news filters (#296)

* fix(cli): preserve candidate slug in news filters

* test(cli): cover candidate news filters and subject switches

* fix(search): deduplicate tags before applying query cap (#298)

* fix(cli): strip closing Markdown hashes from derived job titles (#299)

* fix(cli): accept stdin dash as an option value (rebased #297) (#300)

* fix(cli): accept stdin dash as an option value

* test(cli): cover piped tracker imports and dash parsing

---------

Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>

* fix: withhold contact URIs from anonymous resume views (#302)

* fix: withhold explicit contact URIs from anonymous resumes

Added functions to redact contact URIs and check for contact channels in text.

* fix: guard cached candidate location and skills contact channels

Refactor skill extraction logic to improve handling of category labels and contact channels.

* test: cover encoded contact URIs and anonymous candidate summaries

* test(client): give the fanout timeout test a budget CI can meet (#303)

The healthy mock board shares the 50 ms budget with the slow one, so on a
loaded runner it timed out too and line 32 failed. It caused 3 of the last 4
master CI failures and blocked #301. 1 s keeps the assertion (one slow call,
no budget restart) while leaving the healthy board ample headroom.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): infer job titles from indented Markdown headings (#301)

* fix(cli): recognize indented Markdown job titles

* test(cli): cover indented title extraction and code boundaries

* fix(cli): preserve leading code indentation after front matter

Updated body extraction to remove blank opening lines while preserving indentation for Markdown syntax.

* test(cli): retain initial code examples during title extraction

* fix(mcp): suppress responses to notifications (#304)

* fix(markdown): preserve literal link destinations (#305)

* fix(resume): retain indented headings in candidate discovery (#306)

* feat(webring): Random link in the Profullstack ring footer (#307)

Also serves /.well-known/openwebring.json (agenticjobs.work is member
agenticjobs-work-3 in the ring), which the board did not have.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(footer): use @profullstack/footer (#309)

The board's link row and the Profullstack ring nav move into the shared footer
package, rendered on the server from its @latest template (kept and refreshed
in the background, since Layout renders synchronously). The popular-skills row
stays as the site's own footer above it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(markdown): preserve blank-separated list siblings (#308)

* fix(markdown): strip complete link destinations from summaries (#310)

* fix(footer): drop the skills row's own rule above the shared footer (#311)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(query): drop NUL bytes in string filters instead of answering 500 (#312)

PostgreSQL refuses a NUL byte in text outright, so a single `%00` in any
string filter reached the LIKE pattern and turned the request into a 500:

- `GET /api/v1/jobs?q=%00`, `?org=%00`, `?tags=%00` - and every surface
  that shares parseQuery with it: the page, MCP, the CLI and federated
  search at once.
- `GET /api/v1/directory/instances?q=%00` and `?topic=%00`.

Both filters now pass through the same clean() gate the rest of the board
uses, so the byte is dropped the way an unknown value is dropped rather
than turned into an error, and a filter that cleans away to nothing is
treated as absent rather than as a search for the empty string.

Verified against the live board before the change: `?q=%00` answered 500
on both routes while `%01` and `%1f` answered 200, which is what pointed
at the NUL byte specifically rather than at control characters in general.

Co-authored-by: muazaulia <ojikaminari@gmail.com>

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard the decode that throws (#313)

* fix(core): drop NUL bytes where slugs and ids reach SQL, guard decode

The rest of the class #312 started. PostgreSQL refuses a NUL byte in a
text parameter outright, so every lookup that took a slug or an id
straight from a request answered 500 where an unknown value answers
"nothing here": "org" on POST /jobs and /updates, "job", "as",
"candidate" and "employer" on POST /inbox, "resumeId" and "resumeSlug"
on POST /jobs/:slug/apply, ?candidate=%00 on /inbox/new, ?state=%00 on
the CoinPay callback, and a board link whose path carries %00 - or a
malformed escape, which decodeURIComponent throws on, uncaught until
now.

Drop the byte at each boundary, the way parseQuery drops it from a
search, and treat a value that cleans away to nothing as absent rather
than as a lookup for the empty string. One test per boundary.

* fix(pages): read the logout cookie the way the middleware does

The route parsed the cookie header by hand and ran decodeURIComponent
on the value, so `Cookie: aj_session=%E0%A4%A` - a malformed escape -
threw a URIError out of the route and answered 500 where every token
that matches no session answers the redirect home. Read it with
getCookie, the same helper the middleware uses, which leaves what it
cannot decode alone.

Verified live before the change: the request answered 500. Three tests
against a real Postgres: the malformed escape, a NUL byte, and a real
session still being revoked.

* fix(resume): preserve contact links with trailing destination whitespace (#314)

* fix(resume): accept whitespace before a link closing parenthesis

Preserve contact destinations when valid Markdown links include trailing spaces or tabs. AI-assisted change, reproduced and tested locally.

* Create resume-link-space.test.ts

* fix(docs): OpenCreds and ASDLC links point at logicsrc.com (#315)

/docs/openprofile was copied from logicsrc.com, where /opencreds and /asdlc
are local pages. On agenticjobs.work they 404ed (ThreatCrush Link Watch).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Add a CrawlProof ad unit above the footer on public pages (#316)

One text_link unit served as a cross-origin /api/ads/frame iframe for slot
9c3d6e3d-ae1f-43ff-b6d9-4f42f11725ce. The board's strict CSP gains only
frame-src https://crawlproof.com; no ad.js, no extra script or storage.
Skipped on noindex pages and on flows (tracker, post, me, inbox, auth...).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.5: draft writer falls back to OpenAI, and says when an account is empty (#317)

The /post drafter only ever called one provider, and reported OpenAI's
"no credits remaining" 429 as "rate limited, try again shortly". Now
Anthropic is tried first and OpenAI answers when it fails; an exhausted
balance on either says "out of credits"; the provider's raw answer is
logged server-side (never shown); WRITER_MODEL only goes to the
provider it names.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.6: pit.world global footer; the site footer is two rows (#318)

- src/views/pit-footer.ts replaces pfs-footer.ts: the snippet from
  https://pit.world/snippets/footer.html?site=<board host>, a per-host module
  cache refreshed hourly in the background (3s timeout, last good copy kept,
  nothing rendered if never fetched), last in <body>.
- Footer: the popular-skills row stays; the @profullstack/footer bar (nine
  links, tagline, webring) becomes one row: copyright, Popular, Agents, API,
  Feed, Source. The ring now comes from the global footer.
- The CrawlProof unit moves from above the footer to the top of <main>.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 0.22.7: restore the @profullstack/footer bar under the site footer (#319)

The webring bar is being rolled out fleet-wide on purpose. It comes back with
no extra links (the site footer row carries them), between the site footer and
the pit.world global footer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: exempt all @profullstack/* from minimumReleaseAge (#320)

Our own packages; version-pinned excludes made every fleet bump wait 24h.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* No ads on the homepage; low-profile text-link ads elsewhere (#321)

* No ads on the homepage; low-profile text-link ads elsewhere

Homepage renders no ad unit. Remaining units are a single text_link
line per page with quiet spacing, never against the footer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump the service worker cache VERSION for the app.css change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(invoices): send payer, payee and property to CoinPay; read the nested webhook (#322)

requestPayment now passes customer_email (payer email), account_id
(payer id) and property agenticjobs.work top-level on
/api/payments/create, and the same plus payee_id in metadata. The
peer-invoice model is unchanged: the payer still pays the payee's own
wallet via merchant_wallet_address.

applyWebhook read event/payment_id flat, so a real nested CoinPay
webhook ({id: evt_..., type, data: {payment_id, status}}) was always
ignored and invoices only settled on the next page load. It now reads
data first with the flat shape as a fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(crawl): sell training crawlers a day pass on /crawl (x402-gateway 0.11) (#323)

The crawl gateway runs first when COINPAY_X402_KEY and CRAWL_PAY_TO are
set: GPTBot, ClaudeBot, headless and SEO scrapers get a 402 with
PAYMENT-REQUIRED and the /crawl sales page (x402 USDC or the CoinPay Buy
a day pass checkout) before the board spends a query on them. People,
verified search crawlers, AI chat/search fetchers and aj_session holders
pass through; /api/health stays open. A board without the two settings
never sells.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(profile): shared signup profile fields via @profullstack/profile (#324)

/signup asks for first_name, last_name, company_domain, job_title and
linkedin_url beside the email box (plain /login never does). The fields are
validated before any link is made, ride on the magic link (magic_links.profile)
because the account is created when the link is spent, and are saved into the
package's user_profiles table on /auth/callback. A failed save never blocks
sign-in.

GET/PUT/PATCH /api/v1/me/profile are the package's handlers; /me gets a
private Contact details form. Migration 0024 creates user_profiles and
backfills first/last name from users.name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): upgrade all dependencies (0.22.8) (#325)

* chore(deps): upgrade all dependencies (0.22.8)

@hono/node-server 2, typescript 7, hqtui 0.11.1, x402-gateway 0.13.1, profile
0.1.6, synconfig 0.1.3, hono 4.13.14, pg 8.23.1, simplewebauthn 14.0.3. Bun
runtime image 1.4.4. Boot-tested with a throwaway Postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(signup): count email boxes inside the signup form only

footer 0.3 renders a newsletter form with its own email input on every page,
so a page-wide count found two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(credits): credit codes, invites and ?ref= links (0.23.0) (#326)

A code (AJ-XXXX-XXXX-XXXX-XXXX, stored only as a hash) grants credit in US
cents, redeemed on /me, over the API, the CLI or MCP
(@profullstack/redeem-credits). Every account gets three single-use /i/<code>
invites at /invites plus one open ?ref= link (@profullstack/affiliates).

- The invite and ref ride on the magic link, so a link opened in another
  browser still counts, and they apply only when that link makes a new account
  (the address is confirmed by construction).
- An accepted invite pays both sides INVITE_BONUS_CENTS (default 100) unless the
  two addresses are one mailbox, the inviter's account is younger than
  INVITE_MIN_INVITER_AGE_HOURS (default 24), or the inviter has had ten bonuses
  in 30 days. invite_bonuses claims the row before the grant, so a retry never
  pays twice.
- /i/ is open past the crawl gateway; links are built from PUBLIC_URL, or the
  request host when that is still localhost.
- Operator: agenticjobs credits mint|list|revoke against DATABASE_URL; admin
  POST /api/v1/admin/credit-codes from a browser session.
- The board charges no listing fee or commission, so nothing spends credit yet.
- api.test: retry an agent path once when a parallel test hid the agent.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.1, never charge sign-in or app paths (#327)

Since gateway 0.8 a stale or headless Chrome string was answered 402 on
/login, /signup, OAuth callbacks and /api/auth; 0.15 never charges those.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): @profullstack/x402-gateway ^0.15.2, old browsers get the script challenge (#328)

A desktop Chrome two years behind was answered 402 on every content page;
0.15.2 gives it the script challenge first, so a person on an old office
machine reads.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(coinpay): keep payees' wallet addresses, never their CoinPay tokens (0.23.1) (#329)

coinpay_accounts held every connected person's CoinPay access and refresh
token in plaintext, readable by anyone with the database or root on dev2.
The board only ever used them for one call, /oauth/userinfo, to re-read the
person's receiving wallets on "Refresh wallets". Payments are minted with the
board's own business key, never a user token. Addresses are not secrets, so
keep those and stop storing the tokens.

- The OAuth callback writes the wallets (+ wallets_synced_at) and nulls the
  token columns in the same upsert. "Refresh wallets" is now a fresh consent
  (POST /me/coinpay/refresh redirects to /me/coinpay/connect).
- usable = the grant had wallet:read; no token is needed to invoice.
- Migration 0026: token columns nullable, wallets_synced_at added (additive).
- Legacy rows are converted by a sweep at boot (then every 6h until none are
  left) and by `agenticjobs coinpay-sweep`: one refresh + userinfo, wallets
  written and tokens nulled in one update. A token CoinPay refuses is deleted
  but the wallets read at connect time stay usable. An outage changes nothing.
- LEGACY_KEY_MIGRATION=off never sends a legacy token anywhere again; the
  sweep only deletes them. The sweep reports `remaining`.

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.15.3 (browsers asking for a page are never 402'd) (#330)

Chrome 120-132 and Firefox 130 desktop user agents got the x402 JSON offer
instead of the page; 0.15.3 lets any request that asks like a browser read
it. Declared AI crawlers are still charged. profullstack/x402-gateway#28

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: @profullstack/x402-gateway 0.16.0 (one fetch reads the page) (#331)

A stale, spoofed or headless browser string now reads 20 pages an hour per
address before the 402, so a person or an agent fetching one page gets it
and a bulk crawl still pays. Declared AI crawlers are still charged on
sight. New authenticate hook for sites with API keys.
profullstack/x402-gateway#29

Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(apply): reject a malformed resume id before it reaches Postgres (#267)

* fix(apply): reject a malformed resume id before it reaches Postgres

* fix(apply): reject a malformed resume id before it reaches Postgres

---------

Co-authored-by: RowletCC <chengjacky073@gmail.com>
Co-authored-by: kepler-ops-maker <sungmin@mail.instinct.com>
Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: SII-Zhuangqiu Huang <46834192+hzhuangqiu@users.noreply.github.com>
Co-authored-by: zhonglingzlzl-lgtm <zhonglingzlzl@gmail.com>
Co-authored-by: waytzhang <chao.zhang@crimsoneducation.org>
Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>
Co-authored-by: k1mtruth <56712246+k1mtruth@users.noreply.github.com>
Co-authored-by: zelumi <sun2000feng@gmail.com>
Co-authored-by: ilanvkn <ilan.vkn@gmail.com>
Co-authored-by: bmdy5 <110339793+bmdy5@users.noreply.github.com>
Co-authored-by: xiaoliang <xiaoliang@tinypace.com>
Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>
Co-authored-by: Ayush Agrawal <131985798+ayush-agrawal001@users.noreply.github.com>
Co-authored-by: reacx-research <reacx@proton.me>
Co-authored-by: Dayire <s.rachid.riad@gmail.com>
Co-authored-by: ouji kaminari <kaminariouji@gmail.com>
Co-authored-by: muazaulia <ojikaminari@gmail.com>
Co-authored-by: shkim3142-ops <shkim3142@gmail.com>
Co-authored-by: sh1pt-actions-fleet[bot] <287014002+sh1pt-actions-fleet[bot]@users.noreply.github.com>

* fix(markup): port onto master's protectHtml; keep bare URLs off the sentinel, code spans out of image alt

Master already holds emitted links behind a sentinel (the nested-anchor half of
this PR). What still reproduced: a bare URL followed by a code span was not
linked at all, and a code span inside an image alt restored as <code> tags in
the attribute. Emphasis in alt stays literal, per #305.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: RowletCC <chengjacky073@gmail.com>
Co-authored-by: kepler-ops-maker <sungmin@mail.instinct.com>
Co-authored-by: Anthony Ettinger <anthony@profullstack.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: SII-Zhuangqiu Huang <46834192+hzhuangqiu@users.noreply.github.com>
Co-authored-by: zhonglingzlzl-lgtm <zhonglingzlzl@gmail.com>
Co-authored-by: waytzhang <chao.zhang@crimsoneducation.org>
Co-authored-by: Clearwork Codex <174286206+waytzhang@users.noreply.github.com>
Co-authored-by: k1mtruth <56712246+k1mtruth@users.noreply.github.com>
Co-authored-by: zelumi <sun2000feng@gmail.com>
Co-authored-by: ilanvkn <ilan.vkn@gmail.com>
Co-authored-by: bmdy5 <110339793+bmdy5@users.noreply.github.com>
Co-authored-by: xiaoliang <xiaoliang@tinypace.com>
Co-authored-by: OnePaperclipLab <339823780+OnePaperclipLab@users.noreply.github.com>
Co-authored-by: Ayush Agrawal <131985798+ayush-agrawal001@users.noreply.github.com>
Co-authored-by: reacx-research <reacx@proton.me>
Co-authored-by: Dayire <s.rachid.riad@gmail.com>
Co-authored-by: ouji kaminari <kaminariouji@gmail.com>
Co-authored-by: muazaulia <ojikaminari@gmail.com>
Co-authored-by: shkim3142-ops <shkim3142@gmail.com>
Co-authored-by: sh1pt-actions-fleet[bot] <287014002+sh1pt-actions-fleet[bot]@users.noreply.github.com>
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