The AppImage artifact test only validated package structure (extraction,
AppDir layout, asar contents) — runtime regressions like frame-fix-wrapper
syntax errors, bad asar patches, or Electron startup crashes silently
passed CI. The .deb path already ran `--doctor` as a smoke check; the
AppImage path now has parity plus a 10s headless launch under Xvfb.
`setsid` + `kill -- -PGID` is load-bearing: xvfb-run's EXIT trap leaks
Xvfb on signal kill, so running the whole stack in its own process group
lets the teardown reap xvfb-run, Xvfb, dbus, AppRun, electron, and zygote
children together. `procps` (for pkill), `dbus-x11`, and `xvfb` added to
the CI apt line.
The headless probe catches main-process startup failures only — GPU /
renderer-process crashes like #583 leave the main process alive and pass
this check; that scope disclaimer is inlined at test-artifact-appimage.sh
lines 114-117 so future contributors don't try to claim #583 coverage by
switching Xvfb off.
Co-authored-by: Sum Abiut <sabiut@users.noreply.github.com>
Prevents manual workflow_dispatch invocations from stacking on the
same ref. Uses cancel-in-progress: false to match ci.yml so a
reusable workflow_call invocation inside an in-flight CI run isn't
killed when a new push lands.
Co-authored-by: Sum Abiut <sum.abiut@titanfx.com>
* verify(cowork): static-grep shipped asar for PR #555 markers
D6 of #559's followup plan: post-build check that greps the shipped
app.asar for 9 known cowork patch markers and exits non-zero if any
are missing. Catches the half-patched-asar failure mode from PR #555,
where two of three failed gates had no else branch and the build log
showed "Applied 10 cowork patches" instead of warning.
- scripts/cowork-patch-markers.tsv: single source of truth.
Tab-separated name<TAB>pcre<TAB>sample. Both verify and BATS read it.
- scripts/verify-cowork-patches.sh: accepts a .js, an .asar (npx
@electron/asar extract), or a directory containing
app.asar.contents/.vite/build/index.js. Exits 0/1/2.
- tests/verify-cowork-patches.bats: regex-matches-sample integrity,
positive full fixture, per-marker negative fixtures, input-shape
coverage. 9 new BATS cases.
- .github/workflows/build-amd64.yml: runs verify against the deb
build's asar. Pinned to deb because the patched JS is identical
across formats.
Validated end-to-end against the pinned 1.5354.0 installer:
unpatched -> 9/9 miss; cowork.sh patched -> all 9 present.
Refs #559.
Co-Authored-By: Claude <claude@anthropic.com>
* verify(cowork): share TSV parser between verify.sh and BATS
Realises the library-mode plumbing the previous commit added but
didn't use: BATS now sources verify-cowork-patches.sh and calls
load_markers, so a TSV format change cannot desync the two consumers.
Drops the duplicate parser in tests/verify-cowork-patches.bats.
Also tightens main()'s loop (for over indexed while, drop redundant
missing counter) and the BATS index loops.
Behaviour-preserving; bats tests/verify-cowork-patches.bats still 9/9.
Co-Authored-By: Claude <claude@anthropic.com>
* rename: verify-cowork-patches → verify-patches (generic)
Rename the verify infra to make its generic intent explicit. Per
sabiut's review note on #575, the script + TSV are reusable for
non-cowork patch sets in principle — drop "cowork" from the script
and BATS filenames to reflect that, and accept an optional second
arg for the marker TSV path so other patch sets can plug their own
TSV in without forking the script.
The TSV itself stays cowork-specific (`cowork-patch-markers.tsv`)
because its contents are cowork markers; the script defaults to it
so existing CI keeps working without changes beyond the rename.
Routing implication noted by sabiut: filename now lives under
`/tests/` → @sabiut codeowner mapping (intentionally; the verify
infra is generic). Cowork-specific marker changes still touch the
TSV under `/scripts/`, which routes to @aaddrick/@RayCharlizard via
the cowork-* CODEOWNERS rule.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
DNF defaults to a 48h metadata cache when metadata_expire is unset,
so users running `dnf install/reinstall claude-desktop` shortly after
a release see stale versions until either the cache expires or they
manually run `dnf clean expire-cache`.
Lower the cache TTL on the generated repo file so freshly published
releases propagate within an hour without user intervention.
Co-authored-by: Claude <claude@anthropic.com>
* fix(ci): force primary GPG key for repomd.xml signing
PR #217 added --default-key for the gpg invocation that signs
repomd.xml, but gpg's --default-key only chooses an identity, not
which key under that identity actually signs. Without a trailing
'!' on the keyid, gpg silently picks the most recent signing
subkey. rpm 4.20+ and zypper verify repomd.xml only against the
primary key, so the published signature fails verification with
"Signature verification failed for repomd.xml" / "Signing key not
found" — the exact symptom reported in #213.
Append '!' to the keyid argument to force the primary key.
Verified locally against zypper 1.14.96 / rpm 4.20.1 / gpg 2.x by
re-signing the live repomd.xml with a test primary+subkey keypair:
- Without '!': sig keyid = subkey, zypper refresh fails with
"Signature verification failed for repomd.xml" (reproduces
the production bug 1:1).
- With '!': sig keyid = primary, zypper refresh succeeds:
"Die angegebenen Repositorys wurden aktualisiert."
Fixes#213 (regression of PR #217)
Co-Authored-By: Claude <claude@anthropic.com>
* docs(ci): tighten repomd.xml signing comment
Compress the rationale block from 8 to 6 lines while preserving
the load-bearing facts (gpg picks subkey by default, rpm 4.20+ /
zypper reject subkey-signed repomd.xml, '!' forces the primary
key, #213/#217 regression history). Adds an explicit "Do not
strip it" admonition to the future reader.
No functional change.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Replaces mutable tag refs (e.g. @v4) with full commit SHAs across all
workflows, with the version retained as a trailing comment for
readability and dependabot compatibility.
Motivation: the March 2026 trivy-action supply-chain attack poisoned 75
of 76 version tags in a single repo. Any consumer using @vX-style
references ran the compromised code automatically. SHA pinning makes
that class of attack a no-op for us — a hijacked tag cannot point at
new code without the SHA also changing.
Pinned actions:
actions/checkout@v4, actions/upload-artifact@v4,
actions/download-artifact@v4, actions/setup-python@v5,
actions/setup-node@v4, actions/github-script@v7,
softprops/action-gh-release@v2, crazy-max/ghaction-import-gpg@v6,
codespell-project/codespell-problem-matcher@v1,
codespell-project/actions-codespell@v2,
cloudflare/wrangler-action@v3,
DeterminateSystems/nix-installer-action@v21
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
Quad9 now resolves pkg.claude-desktop-debian.dev to Cloudflare IPs;
the hourly check is no longer needed.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
* ci: run BATS test suite on push and PR
The /tests/ directory has 186 BATS tests
(launcher-common, launcher-xrdp-detection, and four
cowork-*.bats files) but no workflow ever invoked `bats`
— the entire suite was effectively inert.
A regression in launcher-common.sh or
cowork-vm-service.js would not fail any check,
including the BATS suite added by PR #395.
Add a standalone tests.yml workflow that:
- installs bats + nodejs
- runs `bats tests/*.bats`
- executes on every PR
- executes on pushes to main
Push triggers are path-filtered to:
- tests/
- scripts/
- .github/workflows/tests.yml
PR triggers remain unfiltered so required-check
behaviour stays predictable.
Kept this standalone rather than extending
test-artifacts.yml so unit tests run in seconds
instead of waiting for full artifact builds.
This can be promoted to a build gate later once
it proves stable in CI.
CODEOWNERS
- adds /.github/workflows/tests.yml under @sabiut
- keeps /tests/cowork-*.bats ownership with @RayCharlizard
This PR only enables CI coverage for existing tests
and does not modify cowork test logic.
* fix(tests): unset XDG_CONFIG_HOME in cowork-bwrap-config setup
The "doctor: reports custom bwrap mounts" and "doctor: warns
about disabled critical mount /usr" tests failed in CI but
passed locally.
Root cause:
- _doctor_check_bwrap_mounts in scripts/doctor.sh resolves
the config dir via ${XDG_CONFIG_HOME:-$HOME/.config}/Claude
- The test setup() only sandboxes HOME via TEST_TMP
- GitHub Actions runners export XDG_CONFIG_HOME ambient
- Function reads the runner's real config dir, not the test
fixture, and silently emits no output
- Assertions on /opt/tools, WARN, etc. fail
Surfaced by PR #520 wiring BATS into CI for the first time;
the bug existed before but was hidden by the suite never
running.
Fix: unset XDG_CONFIG_HOME in setup() so the function falls
back to \$HOME/.config (which is sandboxed). Comment in the
file documents why HOME alone is insufficient.
Verified: 186/186 pass with XDG_CONFIG_HOME set ambient
(reproduces CI env).
* chore(monitoring): hourly Quad9 DNS check for pkg.claude-desktop-debian.dev
Adds a workflow that fires hourly via cron, runs `dig +short` against
Quad9 (9.9.9.9), and appends a result line to the body of issue #524.
On the first successful resolution, the workflow tags @aaddrick and
self-disables via `gh workflow disable`.
Includes workflow_dispatch so the check can be triggered on demand
without waiting for the next cron tick. Token scope is the default
GITHUB_TOKEN with issues:write + actions:write.
Refs #521#524
Co-Authored-By: Claude <claude@anthropic.com>
* chore(dns-monitor): pass step output through env, not bash interpolation
Routing `steps.dig.outputs.line` through `env:` matches the pattern
used by `apt-repo-heartbeat.yml` and avoids interpolating arbitrary
text directly into the shell command.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Swaps the two markdown blocks so the apt scheme-downgrade signpost is
the first thing a user sees when they open the bug template — the
privacy notice still renders, just below it.
Co-Authored-By: Claude <claude@anthropic.com>
Adds a contact_link on the issue chooser that surfaces the apt
scheme-downgrade symptom verbatim and links the README migration
section, plus a markdown callout at the top of bug_report.yml with
the inline sed one-liner. Catches reports like #516 and #519 before
they're filed as bugs.
Co-authored-by: Claude <claude@anthropic.com>
Phase 4a-APT cutover (#493, #503) moves binary distribution behind a
Cloudflare Worker at pkg.claude-desktop-debian.dev. The Worker serves
repo metadata directly and 302-redirects .deb/.rpm requests to GitHub
Release assets, which makes the >100 MB .deb push cap irrelevant.
GitHub Pages auto-301s legacy aaddrick.github.io/claude-desktop-debian
URLs to pkg.claude-desktop-debian.dev, but the redirect uses http://
(Pages has no cert for pkg.<domain> — DNS points at Cloudflare, so
Pages can never pass domain verification). apt refuses that scheme
downgrade as a security policy, so existing users' sources.list
silently breaks on the next `apt update`. DNF accepts the downgrade
and keeps working.
Changes:
- README.md: install snippets (APT + DNF) now point at
pkg.claude-desktop-debian.dev directly. New users never touch the
Pages redirect chain.
- README.md: add a "Migrating from the old aaddrick.github.io URL"
section with sed one-liners for existing users + a short background
paragraph explaining why the change was needed.
- .github/workflows/ci.yml: release-notes install snippets (APT + DNF,
both branches) and the generated claude-desktop.repo file's baseurl
and gpgkey all point at pkg.<domain>. Smoke-test chain walkers
deliberately keep starting at github.io (they test the full 3-hop
Pages→Worker→Releases chain for clients that do follow the
downgrade, like curl-without-L and dnf).
Refs #493, #503
v2.0.4 rerun of update-apt-repo made it past hops 0 and 1 (the smoke
test scheme fix in #506 worked — Pages' http:// redirect no longer
trips the chain walker), but failed on hop 2:
Hop 2: 302 .../releases/download/v2.0.4+claude1.3883.0/...deb
-> https://release-assets.githubusercontent.com/...
::error::Hop 2 mismatch: expected https://objects\.githubusercontent\.com/,
got https://release-assets.githubusercontent.com/...
GitHub migrated the Release asset CDN from objects.githubusercontent.com
to release-assets.githubusercontent.com (both have been serving in the
past; release-assets is the current canonical hostname). Accept either
hostname via alternation.
Verified against the actual v2.0.4 Release:
$ curl -Is https://github.com/aaddrick/claude-desktop-debian/releases/download/v2.0.4+claude1.3883.0/claude-desktop_1.3883.0-2.0.4_amd64.deb \
| grep -i location
location: https://release-assets.githubusercontent.com/github-production-release-asset/...
Same fix in three sites:
- .github/workflows/ci.yml (update-apt-repo smoke test)
- .github/workflows/ci.yml (update-dnf-repo smoke test)
- .github/workflows/apt-repo-heartbeat.yml (daily heartbeat)
docs/worker-apt-plan.md has historical references to
objects.githubusercontent.com too; those can be updated in a follow-up
docs sweep — the architectural claim (binary bytes flow direct from
GitHub CDN, never through Cloudflare) is unchanged.
Refs #493, #503
* fix(ci): smoke test allows http:// on Pages 301 hop
Phase 4a-APT's first rerun of update-apt-repo succeeded all the way
through strip + push (v2.0.3 metadata is live on gh-pages now), but
the smoke test failed at hop 0:
Hop 0: 301 https://aaddrick.github.io/.../*.deb
-> http://pkg.claude-desktop-debian.dev/.../*.deb
Hop 0 mismatch: expected https://pkg..., got http://pkg...
Pages emits http:// in the Location header because https_enforced is
unsettable on the repo's Pages config: DNS for pkg.<domain> points at
Cloudflare (Worker custom_domain), so Pages can never pass domain
verification to provision its own cert. Cloudflare serves both schemes
for pkg.<domain>, so the http vs https in Pages' redirect is cosmetic
— the chain still terminates correctly.
Relax hop 0's regex in both smoke tests (update-apt-repo,
update-dnf-repo) and the heartbeat workflow to accept https?://.
Later hops stay https-only since GitHub's Release-asset redirects
are always HTTPS.
Failure was the tail-end of run 24836419696's rerun:
https://github.com/aaddrick/claude-desktop-debian/actions/runs/24836419696
Refs #493, #503
* chore: retrigger CI (previous trigger lost to GH flake)
Once the CNAME file is in place on gh-pages (Phase 4a-APT), GitHub
Pages auto-301s all aaddrick.github.io/claude-desktop-debian/* traffic
to pkg.claude-desktop-debian.dev/*. The Worker's origin fetch against
aaddrick.github.io gets 301'd by Pages, the 301 passes through to the
client, the client follows it back to pkg.<domain>, and the Worker
runs again — infinite loop.
Observed immediately after merging #503 and Pages finishing the CNAME
build:
$ curl -I https://pkg.claude-desktop-debian.dev/dists/stable/InRelease
HTTP/2 301
location: http://pkg.claude-desktop-debian.dev/dists/stable/InRelease
x-github-request-id: 3C94:286425:...
x-served-by: cache-yyz4566-YYZ
via: 1.1 varnish
(Scheme-downgrade to http is a separate Pages quirk when
https_enforced=false, which is the case here because DNS points
at Cloudflare, not Pages, so Pages can't provision a cert.)
raw.githubusercontent.com serves the same gh-pages branch content
without Pages' routing layer. All five metadata paths verified to
return 200:
/dists/stable/InRelease
/dists/stable/main/binary-amd64/Packages
/KEY.gpg
/rpm/x86_64/repodata/repomd.xml
/rpm/x86_64/repodata/repomd.xml.asc
Also fixes the deploy-worker.yml post-deploy probe which still
hardcoded pkg-staging. That's what made #503's deploy show as
failed in the Actions UI even though the wrangler deploy itself
succeeded — route bound and Worker live, but the probe was
resolving a hostname wrangler had just removed.
Refs #493, #503
Co-authored-by: Claude <claude@anthropic.com>
Fix#500: rpmsign --addsign mutates RPMs in place, so the Release
asset uploaded by the release job (unsigned) diverged from the
signed copy in gh-pages. The Worker redirects to the Release asset,
so dnf saw a sha256 that didn't match repodata. Re-upload the signed
RPMs to the Release via gh release upload --clobber after signing.
Fix#501: The imported GPG keyring contains two keys; reprepro signs
InRelease with one and rpmsign signs repomd.xml.asc with the other,
but the published KEY.gpg only contained one of them. Strict clients
like rockylinux:9 rejected repo metadata with "Bad GPG signature".
Export the full keyring (all public keys) to KEY.gpg so both
signatures verify.
Validation (per issue reproduction steps):
- Re-run update-dnf-repo on a test tag
- sha256 of gh-pages RPM must match the Release asset download
- fedora:latest dnf install should succeed (was "All mirrors tried")
- rockylinux:9 dnf makecache should succeed (was "Bad GPG signature")
Co-authored-by: Claude <claude@anthropic.com>
* feat: APT/DNF Worker scaffolding (#493)
Adds the implementation scaffolding for the Cloudflare Worker that
fronts the APT/DNF repo, per docs/worker-apt-plan.md.
New files:
- worker/src/worker.js: redirects /pool/.../*.deb and /rpm/*/*.rpm
to GitHub Release assets via 302; passes metadata through to
the gh-pages origin
- worker/wrangler.toml: bound to pkg-staging.claude-desktop-debian.dev
initially; Phase 4a switches to pkg.claude-desktop-debian.dev
- .github/workflows/deploy-worker.yml: deploys Worker on worker/**
push, post-deploy probe verifies route bound + Worker responding
- .github/workflows/apt-repo-heartbeat.yml: daily cron, deb+rpm
matrix, walks ordered redirect chain + size match against Releases
asset, opens format-specific tracking issue on failure (auto-close
on recovery), gates on Worker liveness (skips silently before
Phase 4a)
Modified:
- .github/workflows/ci.yml: gated strip step + ordered-chain smoke
test added to update-apt-repo and update-dnf-repo; the destructive
strip only fires when the production Worker probe succeeds, so this
PR can land before Phase 4a without affecting current behavior
- docs/worker-apt-plan.md: bake in real domain values, mark Decisions
table entries as concrete, fix Cloudflare API token permissions
list (current names: Workers Scripts Edit, Account Settings Read,
Workers Routes Edit; previous "Zone:Zone:Read" name no longer
matches the dropdown)
Pre-Phase-4a behavior: the strip step's liveness probe targets the
production hostname which doesn't exist yet, so it always skips and
.debs/.rpms are pushed to gh-pages exactly as today. Smoke tests skip
on the same gate. Heartbeat workflow's gate skips before the Worker
is live. Nothing destructive happens until Phase 4a explicitly cuts
the Worker over to production.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor: simplify worker scaffolding per cdd-code-simplifier review
- worker.js: use named capture group `asset` instead of opaque `m[1]`
positional reference; inline single-use `tagFor()` helper; demote
unused `arch` capture to non-capturing group.
- ci.yml: hoist `WORKER_DOMAIN` from per-step env to job-level env in
both `update-apt-repo` and `update-dnf-repo` (matches the pattern
already used in `apt-repo-heartbeat.yml`).
- apt-repo-heartbeat.yml: use github-script's native `context.serverUrl`
/ `context.runId` instead of reconstructing from process.env; spread
`...context.repo` instead of repeating owner/repo on every API call;
destructure `{ data: open }` to flatten `open.data` references.
All changes preserve behaviour. The contrarian-fix mechanisms (positive
Worker liveness probe gating the strip step, hop-by-hop ordered chain
walk in smoke tests) are unchanged. APT/DNF strip + smoke pairs remain
in-place per reviewer-readability preference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reporter on #481 pasted the deb package version `claude-desktop
1.3561.0-2.0.0`. The classifier extracted `1.3561.0-2.0.0` verbatim,
and the naive `claimed != CLAUDE_DESKTOP_VERSION` string compare
flagged drift against `1.3561.0`. The issue is on the current
release — no drift should fire.
Fix normalizes both sides: strip a leading `v`, then strip anything
from the first `-` or space onward. Handles:
- `1.3561.0-2.0.0` → `1.3561.0` (deb package: upstream-REPO_VERSION)
- `v1.3561.0` → `1.3561.0` (copy-paste with prefix)
- `1.3561.0 stable` → `1.3561.0` (whitespace-separated qualifier)
- `1.3561.0` → `1.3561.0` (bare upstream, unchanged)
Same normalization applied to CURRENT_VERSION for symmetry, even
though the repo variable is always the bare upstream semver — keeps
the compare resilient if that ever changes.
Fixes the false drift banner on #481 and prevents the same shape
from tripping on any future issue where a reporter pastes their
`dpkg -l | grep claude` output or AppImage filename.
Co-authored-by: Claude <claude@anthropic.com>
The README was drafted as a design spec before implementation. Now
that the pipeline is live and the design has been validated end-to-
end, bring the doc into agreement with the code and retire the two
companion files.
README updates:
- Intro: state the production trigger (`issues: [opened]`) and the
workflow_dispatch fallback; note v1 is manual-only
- Stage 7 table: reorder by actual priority (drift is no longer a
top-of-gate veto); drift section rewritten to describe the banner-
and-candidates-modifier behavior landed in PR #476
- Stage 8a rendered-output example: show the conditional drift
banner + drift-bridge candidates block that actually render
- Stage 8b reason enum: add `reference-source unavailable` that was
missing from the list
- Rollout posture: describe the cutover as completed, not deferred
- Implementation layout: drop "during rollout" qualifier; add
helper-scripts row (validate.sh / drift-bridge.sh /
suspicious-input-scan.sh / extract-json.py)
- Artifacts list: full set with 14-day retention, not just the
original four
- Reasons.json SSOT pointer: actual path `.claude/scripts/reasons.json`
instead of the aspirational `lib/templates/reasons.json`
- Potential future improvements: drop "Cutover to issues:[opened]"
subsection (done)
- Clean up "v1" usage where it means "first version of the pipeline"
(confusable with legacy v1 workflow)
Deleted:
- docs/issue-triage/implementation-plan.md — phased build sequence
is complete; commit history preserves the record
- docs/issue-triage/research-trail.md — design-pass sources are cited
inline in the README where needed
Workflow banner updated to drop the `implementation-plan.md` pointer.
Co-authored-by: Claude <claude@anthropic.com>
Three changes bundled because they land together as the cutover:
1. **v2 `issues: [opened]` trigger enabled.** Workflow now fires
automatically on new issues in addition to the existing
workflow_dispatch path. `run-name`, `concurrency.group`, and the
gate step's ISSUE_NUMBER all resolve via
`github.event.issue.number || inputs.issue_number` so both
trigger paths work. The existing `inputs.dry_run != true` gates
on label/comment application — under an issues trigger that
expression is empty ≠ true, so production posts/labels land.
2. **v1 `issues` trigger removed.** `issue-triage.yml` keeps
`workflow_dispatch` for manual fallback (maintainer can still
fire it if v2 is paused or rolled back), but no longer runs
automatically. v1's `run-name`/concurrency dropped the now-dead
`github.event.issue.number` fallback.
3. **Investigate timeout 600s → 1200s.** Bumped after two
consecutive timeouts on #311 during Phase 4 + drift-as-banner
verification. The investigator needs more tool-call budget on
complex issues. Review step stays at 600s — it runs without
tool access and has never timed out.
Rollback: revert this commit to restore v1's automatic trigger;
v2's `issues:` block goes back to workflow_dispatch-only in the
same operation.
Co-authored-by: Claude <claude@anthropic.com>
Post-Phase 4 verification showed two issues (#311, #448) where the
pipeline successfully produced valuable findings against current
code, but the top-of-gate drift veto routed them to 8b drift-only
and the findings were discarded. The reporter cited an older version
(1.1.7464 on #311), the investigation ran cleanly on current
(1.3.5610), and the reviewer approved the findings — yet the comment
still read "couldn't reach a confident read."
This change keeps drift detection and keeps the drift-bridge sweep.
What changes is Stage 7: drift is no longer at the top of the gate.
When drift is detected and 8a or 8c would render cleanly, the
renderer prepends a drift banner (⚠ You reported this on X; bot
investigated on Y. Citations may still apply.) and appends the
drift-bridge-candidates block at the bottom. The finding citations
stand — they describe current code in hypothesis voice, which is
what the reader can verify against their own checkout.
When drift is detected and the pipeline would otherwise route to 8b
for any other reason (fetch-failure, invest-failure, review-failure,
no-findings, low-confidence), the reason is overridden to
`version-drift`. Drift-bridge candidates give the maintainer a more
actionable signal than "no findings" on its own.
Reviewer prompt gains one rubric addition: downgrade-confidence when
the cited surface clearly post-dates the reporter's version. Catches
the case where a finding is valid on current but wouldn't reproduce
on what the reporter saw. Doesn't degrade findings indiscriminately
— only when the reviewer can see version-specific evidence.
Confirmed-duplicate routing wins over the drift-reason override
(explicit exclusion in the override clause) because `triage:
duplicate` is still the more specific read.
Co-authored-by: Claude <claude@anthropic.com>
* feat(triage): Phase 4 sub-PRs 3+4 — regression_of + edit-during-triage
Bundles the two remaining Phase 4 sub-phases. Both are small workflow
additions that build on infrastructure already in place: the Phase 1
input snapshot (updated_at captured at Stage 1) and the Phase 1
classify.json's regression_of field.
regression_of end-to-end (Stage 3b + Stage 4 + Stage 6)
- New step `Validate regression_of` between drift-check and fetch.
Runs only when classify set regression_of to non-null.
- Validation: PR exists in this repo; PR is merged; PR's mergedAt
precedes issue's createdAt. Any failure clears to null with a
logged note and the issue proceeds as a regular bug.
- Valid regression → `gh pr diff` fetched (capped at 4000 lines) and
inlined into the investigate prompt as primary context. Tells the
investigator to start the search in the PR's changed files.
- Same diff inlined into the review prompt, wrapped as pipeline_data,
so the reviewer can check whether findings land inside the named
PR's changed files.
- Handles the spec's "cleared to null with logged note" requirement
for upstream Electron PRs that aren't in this repo.
Edit-during-triage detection (Stage 8 post-processor)
- New step between 8a/8c post-processors and Apply labels. Runs for
every variant.
- Re-fetches issue.updated_at live and compares against the Stage 1
input_snapshot.updated_at.
- On mismatch: appends a `⚠ This issue was edited after triage
began. ...` disclaimer to the rendered comment, pointing at
input_snapshot.json as the audit trail.
- Catches inject-then-delete attacks (inject instructions, wait for
bot, delete before a human reads) and honest mid-triage edits
that would make the comment stale.
Step summary gains `regression_of validated` row.
With this PR, Phase 4 is complete: 8c enhancement-design, suspicious-
input tells, regression_of, edit-during-triage detection are all
live. All terminal paths (bug / enhancement / question / duplicate /
needs-info / not-actionable / suspicious) flow through the pipeline
end-to-end per spec.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(triage): correct stale sort -u reference in date-compare comment
The comment above the ISO 8601 date check referenced `sort -u`,
which isn't used in the code. Rewrite to describe what the code
actually does: `[[ > ]]` on the raw timestamp strings, which is
valid because ISO 8601 sorts lexicographically as chronologically.
Also re-orient the prose around the invalid case (mergedAt AFTER
createdAt), matching the branch that the following `if` takes.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* feat(triage): Phase 4 sub-PR 2 — suspicious-input tells
Adds a conservative Stage 2a tripwire that scans the raw issue body
and title for prompt-injection tells before any LLM call. A match
short-circuits routing to 8b with reason
`suspicious-input — manual review`, no Sonnet invocation.
The scan is the front-line filter; the actual injection mitigations
(wrap-as-data, fresh-context reviewer, schema-constrained output)
remain in place for everything that doesn't trip. The two layers are
complementary: the scan catches the obvious attempts cheaply, the
downstream defenses protect against the clever ones.
Taxonomy
- taxonomies/suspicious-input-tells.json — eight tells with regex
patterns and rationale:
- ignore-prior-instructions: classic opener
- system-prompt-leak: exfiltration attempts
- role-override: "you are now a different…"
- forget-instructions: variation of ignore-prior
- developer-mode: named jailbreaks (DAN, etc.)
- instruction-injection-sysrole: chat-template tokens
- long-base64-block: 200+ contiguous base64 chars
- unicode-tag-sequence: U+E0000-E007F invisibles
Scanner
- scripts/triage/suspicious-input-scan.sh — pure bash, PCRE via
grep -Pzi, writes suspicious-input.json with matched_tells[].
Uses the same taxonomy-as-data pattern as reasons.json and
label-blocklist.json.
Workflow
- Stage 2a step runs between input snapshot and classify, outputs
`suspicious` boolean
- Classify + doublecheck both `if:`-gated so they skip on a hit
- Decide route takes suspicious first, before the doublecheck
disagreement check — a tripped tell defers deterministically
- Step summary shows the suspicious flag
Co-Authored-By: Claude <claude@anthropic.com>
* refactor(triage): drop dead null-string guards in suspicious-input scan
jq -r '.body // ""' already returns an empty string for JSON null or a
missing field, so the subsequent `[[ "${body}" == "null" ]]` guards only
fire when a reporter's body is the literal four-character string "null"
— which isn't an injection signal and matches no tell. The comment
describing the guards was also wrong about jq's behavior. Remove both
guards and correct the comment.
Also fix a misleading comment about `|| true` (which isn't in the code)
and collapse the 4-line `suspicious` boolean derivation into a single
`jq 'length > 0'`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(triage): Phase 4 sub-PR 1 — Stage 8c enhancement-design variant
Adds the third Stage 8 template variant. Previously, enhancement-
classified issues fell through to 8b human-deferral; now they run
through the investigate pipeline with enhancement-specific prompts
and render a lightweight acknowledgment + existing-surface citations
+ design-review questions from a fixed taxonomy.
Prompts and schemas
- taxonomies/enhancement-design-questions.json — six fixed IDs:
config-schema-stability, backward-compat, security-surface,
test-coverage, observability, packaging-format. Each carries a
concrete question the renderer surfaces verbatim.
- schemas/comment-enhancement.json — structured output: 1-sentence
acknowledgment_line, 0-3 existing_surfaces (each with file:line),
1-3 design_question_ids (enum-matched against the taxonomy).
- prompts/comment-enhancement.txt — drafter prompt, hypothesis
voice, rules of thumb for picking design questions.
- prompts/investigate-enhancement.txt — investigate variant. Same
schema, but claim_type=absence is banned (by definition the
enhancement's capability is absent; restating is redundant and
tips into design-prescription). Findings must cite existing code
the enhancement would touch.
- prompts/review-enhancement.txt — reviewer rubric reframed from
"is this defect claim correct?" to "is this an existing surface
the enhancement would actually touch?" Reject leans on
real-but-irrelevant surfaces, since those actively misdirect.
Workflow
- Route decision: enhancement now enters the investigate path
alongside bug and duplicate (route renamed `investigate`). Both
the investigate step and the review step pick the enhancement-
variant prompt when classification == enhancement.
- Decision gate: new enhancement branch slotted between
invest-failure and no-findings. 8c fires when review succeeded
(any kept count, including 0) OR when findings_passed was 0 and
the review step was skipped by design — the design questions
carry the comment alone.
- Stage 8c render: bash cross-joins design_question_ids against
the taxonomy; a missing lookup errors loudly rather than
silently dropping.
- 8c post-processor: 350-word cap per spec; trims the last
existing_surfaces bullet when over cap.
- Apply labels: 8c variant → `triage: investigated` +
`enhancement` class label.
Deferred to later Phase 4 sub-PRs: suspicious-input tells,
regression_of end-to-end diff fetch, edit-during-triage detection.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor(triage): reuse classify step output instead of re-parsing classification.json
Drops two redundant `jq -r '.classification' /tmp/triage/classification.json`
calls in the investigate + review steps; both now read the value via a
`CLASSIFICATION_NAME` env var sourced from `steps.classify.outputs.classification`.
Matches the `Decide comment variant` step's existing pattern for
reading classify state, so the three call sites converge on one idiom.
No behavior change — the prompt-selection conditional reads the same
value; just fewer forks of jq.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the v2 classifier vocabulary with the repo's GitHub label
vocabulary. Previously `classification=feature` was mapped to label
`enhancement` at Stage 9 — a redundant indirection that also caused
miscalibration on defects framed as enhancement-shaped asks (e.g.
#448 "breaks in-app schedulers and 'minimize to tray' expectation"
classified as feature + ambiguous when the maintainer read is bug).
Changes:
- classify.json enum: feature → enhancement
- classify-doublecheck-bugfeature.{json,txt} → classify-doublecheck-bug-vs-enhancement.{json,txt}
- Doublecheck rubric tightened: added "breaks X" / "stopped working"
as explicit bug signals and a rule that a broken expectation wins
over enhancement-shaped framing when both are present. Reduces the
chance of #448-shaped defects routing to the ambiguous bucket.
- investigate.txt absence-claim ban: "feature X is missing" →
"capability X is missing"
- reasons.json: "ambiguous bug/feature classification" →
"ambiguous bug/enhancement classification"
- Workflow: doublecheck step renamed, classification checks updated,
class_label map collapsed to direct (no more feature→enhancement
remap).
- docs/issue-triage/{README.md,implementation-plan.md}: vocabulary
updated throughout (~47 occurrences). 8c variant renamed
Feature-design → Enhancement-design. Planned Phase 4 file names
(comment-enhancement.json, enhancement-design-questions.json)
follow suit.
Kept as-is:
- `.github/ISSUE_TEMPLATE/feature_request.yml` filename — preserves
the GitHub convention reporters recognize on the issue-chooser page;
classifier buckets issues filed through it as `enhancement`.
- v1 `issue-triage.yml` + `triage-classify.json` — untouched; v1 is
slated for replacement and doesn't gain from this rename.
No behavioral change at runtime beyond the rubric tightening; the
rename collapses an indirection rather than adding logic.
Co-authored-by: Claude <claude@anthropic.com>
Gives @sabiut review ownership of /tests/, /scripts/doctor.sh, and the
test-artifacts + test-flags workflows. Shared review with @aaddrick on
/docs/TROUBLESHOOTING.md and /.github/workflows/shellcheck.yml.
Cowork override at the bottom of the file still wins for
/tests/cowork-*.bats per last-match-wins.
Announcement: #467
Co-authored-by: Claude <claude@anthropic.com>
* feat(triage): Phase 3 — Stage 6 adversarial reviewer + duplicate gate
Adds a fresh-context reviewer between mechanical validation (Stage 5)
and the decision gate (Stage 7). The reviewer steel-mans each surviving
finding, commits to a counter-reading, runs closed-world checks on
identifier claims, and emits approve / downgrade-confidence / reject
with structured rationale. It also rates each cited related_issue and
the duplicate_of target (exact / related / unrelated).
Stage 7 now gates on reviewer verdicts. approve keeps a finding at full
confidence; downgrade-confidence keeps it but subtracts 1 from its
contribution to the avg-confidence threshold (floor 0.5); reject drops
it. A new duplicate gate (between fetch-failure and invest-failure in
the priority table) fires when classification == duplicate and the
reviewer rated duplicate_of exact or related — routing the issue to 8b
with 'likely-duplicate-of-#N' as reason and 'triage: duplicate' as
label. An 'unrelated' rating discards the duplicate claim and the
remaining gates apply to the regular investigation output.
- schemas/review.json — reviewer verdict schema, per-finding rationale
required, closed_world_check object for identifier claims, ratings
for related_issues and duplicate_of
- prompts/review.txt — adversarial-reviewer prompt per spec §6; input
is source excerpts + claim + closed_world_options + cited-issue
bodies + duplicate_of body, wrapped as untrusted data; excludes
draft comment, free-form reasoning, and voice instructions
- Workflow: fetch duplicate_of body (inline step), Stage 6 review
call (schema-constrained, no tool access, timeout 600s,
--max-budget-usd 1.50, extract-json fallback on prose), reviewer-
aware filter step, expanded decision gate, triage: duplicate label
path with class inheritance from the target issue (PR #459 item 8),
<pipeline_data> wrappers on 8a-render inlined JSON (PR #459 item 3)
- Route duplicates through investigate pipeline so Stage 5 + Stage 6
can rate the target (previously deferred straight to 8b)
See docs/issue-triage/{README.md §6-§7, implementation-plan.md §Phase 3}.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor(triage): simplify Phase 3 verdict summary step
Two small cleanups in the Stage 6 / "Apply reviewer verdicts" plumbing
that don't touch load-bearing behavior (errexit guards, --slurpfile
cross-join, schema fallback, gate priority, prompt-injection wrappers
all preserved):
* Drop the unused dup_num step output — no consumer references
steps.dup_fetch.outputs.dup_num; Resolve reason text reads
.duplicate_of directly from classification.json.
* Collapse the dup_rating jq filter to a single-line
.duplicate_of_rating.rating // "none" — jq already treats
null.rating as null, so the explicit if/else was just ceremony.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-dispatch of #394 showed the full drift-routing path works end-to-
end except for the post-processor word-cap: base 8b comment is ~50
words, drift-bridge-candidates block adds ~130 words for 10 bullets,
privacy note another ~30 when the reporter is first-time. Actual was
189 words vs 150 cap.
Spec §8b note already flagged this: "Verify length is under 150 words
(account for optional drift-bridge-candidates block)." The parenthetical
acknowledged the block expands the comment, but the original 150 was
the base-comment budget and was never adjusted when the drift-bridge
extension landed in Phase 2.
300 covers the observed worst case (~190) with headroom for edge cases
(long PR titles, longer commit subjects, future drift-bridge output
growth) while still bounding the comment at something scannable.
Capping the drift-bridge render at N entries is a separate concern —
deferred in favor of raising the limit first.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-dispatch of #394 confirmed the 300s timeout bounds the step, but
also exposed a second bug: the step failed with exit 124 instead of
falling through to 8b gracefully. Downstream steps (Decide / Render /
Label / Post) were all skipped, and the raw/payload/stderr archives
that the earlier hardening created were never written because the
shell aborted at the assignment before `printf > investigate-raw.json`
could run.
Root cause: GHA's default shell is `bash -e {0}` (errexit). With
errexit on, a failing command substitution:
raw=$(timeout 300s claude -p ...)
propagates the exit code and aborts the script BEFORE `claude_exit=$?`
runs. My prior assumption that assignments were exempt from errexit
under `bash -e` was wrong in this shell configuration.
## Fix
Use the if-form, which is the only reliable way to catch a failing
command substitution under `bash -e`:
if raw=$(timeout 600s claude -p ... 2>log); then
claude_exit=0
else
claude_exit=$?
fi
A timeout (exit 124) or other CLI failure now sets `claude_exit`,
writes the archived artifacts, and falls through to 8b with a
specific warning — exactly the graceful path the earlier PR intended
but errexit short-circuited.
## Also bumped timeout 300s → 600s
The original 300s was chosen to be "typical investigate runtime + a
bit." Observed times: #424 ran 218s, #442 ran 220s — so 300s left
almost no headroom. Doubling to 600s gives room for complex issues
to converge while still being short of the ~9-minute hang that
motivated the timeout in the first place.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The investigate call was the only Sonnet invocation in v2 without
`--json-schema`. After the parser hardening in #461, re-dispatched
runs produced valid JSON — but with fields omitted and creative
top-level wrappers. The prompt-described schema isn't enforced
without the flag, and the model was using the freedom.
## What changed
Add `--json-schema "${schema}"` where `schema=$(cat
.claude/scripts/schemas/investigate.json)`, matching the classify
and doublecheck pattern.
Output parsing prefers the CLI-validated `.structured_output` field
(populated when schema fit cleanly), falling back to the existing
`.result` + `extract-json.py` + shape-check path for the case where
the CLI returns prose on schema miss. The hardened extraction from
#461 stays in place as the safety net.
## Why post-hoc still helps
Per Claude Code CLI docs (and confirmed via the claude-code-guide
research), `--json-schema` applies validation after the agent loop
ends — not at generation time. That's weaker than the Agent SDK's
constrained decoding, but still catches the specific failures seen
in the re-dispatch of #424 and #442:
- Top-level `pattern_sweep` and `proposed_anchors` omitted
- Per-finding `confidence` / `line_end` returned as null (violates
required enum / integer)
- Extra top-level fields like `summary`, `classification`,
`investigation_id`
If post-hoc validation isn't enough, the next escalation is the
Agent SDK (constrained decoding via grammar compilation).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three failure modes surfaced in the first round of dispatches against
real issues, all in the Stage 4 Investigate step:
- #394 hung for 9 min (the Claude CLI wedged; no per-call timeout);
user had to cancel manually. Step log was silent because
`2>/dev/null` swallowed stderr.
- #424 and #442 both ran to CLI completion but the payload's jq
presence-check rejected the output. Raw response wasn't archived,
so the specific rejection cause was unknowable post-hoc.
## Fix
- `timeout 300s claude -p ...` — bounds the step at 5 min; exit 124
routes to 8b no-findings gracefully via the existing warning branch.
- `2>/tmp/triage/investigate-stderr.log` instead of `2>/dev/null` —
CLI diagnostics ride along in the run's uploaded artifact bundle,
available for post-mortem without a re-dispatch.
- Raw CLI response archived as `investigate-raw.json` before any
parsing. Extracted payload archived as `investigate-payload.txt`
before schema checks. Schema-reject no longer loses the evidence.
- Fence-strip + jq-presence-check replaced with
`.claude/scripts/triage/extract-json.py`, which uses
`json.JSONDecoder.raw_decode` to handle leading OR trailing prose
around the JSON body. Addresses PR #459 review item 6.
- The shape check now verifies each of the four required fields is
an `array`, not just present — `{"findings": "oops"}` would pass
presence and explode downstream. Addresses PR #459 review item 7.
## Testing
`extract-json.py` exercised locally against: bare JSON, leading
prose, trailing prose, fence-wrapped JSON, pure prose (exit 1),
malformed JSON (exit 2). All cases produce the expected output or
exit code.
`actionlint -shellcheck` clean on the workflow.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dry_run dispatch input so the pipeline can be validated against
real issues without writing to the repo. Also folds in three items
from the #459 code review that are easier to ship before the first
round of dispatches than after.
## dry_run
- New boolean input on `workflow_dispatch` (default false)
- Guards `Apply labels` and `Post comment` steps
- Step summary shows a ⚠ banner + a "Dry run" row when enabled
- Artifacts still upload, so the rendered `comment.md` is inspectable
## Review fixups (from PR #459 review)
1. **Decision gate priority.** Spec §7 puts version drift ahead of
fetch failure; implementation had them reversed. When both fire,
`version-drift` is the more specific signal and is the only path
that hands the maintainer drift-bridge candidates. Swapped.
2. **Issue titles wrapped as untrusted.** `<issue_title>` now carries
`source="reporter, untrusted"` in all three prompt assemblies
(classify / doublecheck / investigate). Instruction-as-data
directive in each prompt updated to name both `<issue_title>` and
`<issue_body>`. Reporter-controlled title injection surface closed.
5. **`drift-bridge.sh` version search is literal.** `--fixed-strings`
added to `git log --grep` so `1.3.23` doesn't match `1x3y23`.
Items 3, 4, 6-9 from the review are deferred to Phase 3 (adversarial
reviewer) per the review's own scoping.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Directory scaffolding + skeleton workflow + issue templates. No live
behavior — v2 remains workflow_dispatch-only with `permissions: {}` and
a single job that echoes the issue number. v1 (`issue-triage.yml`) is
untouched.
Per docs/issue-triage/implementation-plan.md Phase 0:
- `.github/workflows/issue-triage-v2.yml` — skeleton workflow
- `.github/ISSUE_TEMPLATE/{config,bug_report,feature_request}.yml` —
shapes input for the Stage 2 classifier and Stage 4 investigator;
privacy disclosure in a non-editable markdown info block
- `.claude/scripts/prompts/.gitkeep` — prompts land per-phase
- `.claude/scripts/taxonomies/label-blocklist.json` — Stage 9 suggested-
label gating (wontfix, invalid, duplicate, help wanted, good first
issue); additional taxonomies land in Phase 4
- `.claude/scripts/reasons.json` — Stage 8b deferral-reason SSOT
consumed by the renderer and post-processor (six entries)
- README Privacy section — keeps disclosure text discoverable without
filing an issue; matches the templates' info block
Exit criteria: dispatch against any issue number prints correctly; no
API calls, no comments, no labels; `bug_report.yml` / `feature_request
.yml` render cleanly with the privacy block.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the inline prompt text that guides the triage investigation
agent so it looks for patches in the correct location. The previous
prompt told the agent "search build.sh for patch_ functions" — those
functions have moved into scripts/patches/*.sh organized by subsystem
(tray, cowork, claude-code, quick-window, titlebar, app-asar).
Without this, the triage agent would open build.sh, find only the
orchestrator's source statements, and fail to locate the actual
patch logic — producing lower-quality diagnoses.
Three prompt blocks updated: the "How This Project Patches" section,
the "All bugs are ours to fix" checklist, and the "Patch Approach"
output format. build.sh itself still appears as the orchestrator
reference for context.
Co-Authored-By: Claude <claude@anthropic.com>
The auto-version-bump workflow greps/seds against the Claude Desktop
download URLs and SHA-256 checksums. With the build.sh split those
declarations now live in scripts/setup/detect-host.sh inside
detect_architecture's case statement.
Without this fix, the next upstream release triggers the workflow
and it silently fails to update either the URLs or the checksums
(greps return empty, seds match nothing, git diff finds no changes,
no commit, no tag).
Updates all 17 references — grep targets, sed targets, git
diff/add paths, and step labels / echo messages for consistency.
The patterns themselves (x86_64) / aarch64) case matching,
claude_download_url=' extraction, in-range claude_exe_sha256
replacement) are unchanged and still match the new file's content.
Co-Authored-By: Claude <claude@anthropic.com>
Passes -x (--external-sources) to shellcheck so it follows the
'# shellcheck source=...' directives in build.sh and checks the
split modules in their sourced context. Without this, every sourced
module triggers SC1091 (can't follow source) plus SC2154/SC2034
noise from cross-file variable usage.
Also quotes $script_dir inside $(dirname $script_dir) in
scripts/packaging/rpm.sh — the heredoc-embedded command
substitution tripped SC2086 once shellcheck started analyzing the
subshell context.
Co-Authored-By: Claude <claude@anthropic.com>
Groups the repo into logical roles (build orchestration, setup,
electron patches, desktop integration, staging, packaging,
distribution, CI, docs) with @aaddrick as default. Cowork paths
route to @RayCharlizard; nix paths route to @typedrat.
Overrides are listed after broad globs so last-match-wins resolves
in the intended direction (e.g. docs/cowork-*.md is claimed by
@RayCharlizard after the broad /docs/ assignment).
Pairs with the scripts/ subdirectory layout landed in the previous
commits — each logical role maps cleanly to a path prefix.
Co-Authored-By: Claude <claude@anthropic.com>
Bypasses the AI-powered compare-releases step to reduce API costs.
Falls back to the existing generic release notes template.
Co-Authored-By: Claude <claude@anthropic.com>
The OOM fix is working — the script survives the full pipeline now. But
498 hunks of Claude-powered analysis need more than 5 minutes. Increase
timeout to 180 minutes so AI-generated release notes can complete. The
fallback and if: always() hardening remain as safety net.
Co-Authored-By: Claude <claude@anthropic.com>
OOM fix is in progress in claude-desktop-versions. Re-enabling so the
next release tests the fix. The if: always() hardening on fallback and
release steps ensures the release still ships if the script fails.
Co-Authored-By: Claude <claude@anthropic.com>
The concurrency group fix was insufficient — the runner SIGTERM occurs
even with a single CI run. The compare-releases.py script itself causes
the runner to die (~86s, exit 143) regardless of concurrency. Disabling
the step entirely until the script is debugged in claude-desktop-versions.
The fallback notes and if: always() hardening remain in place.
Co-Authored-By: Claude <claude@anthropic.com>
Add concurrency group to CI workflow so concurrent runs (triggered when
check-claude-version pushes to main then pushes a tag) queue instead of
killing each other. This addresses the ~86-second runner SIGTERM that
has blocked 10 releases in March.
Also harden release steps as defense-in-depth:
- timeout-minutes: 5 on compare-releases step
- if: always() on fallback notes and Create GitHub Release steps
Co-Authored-By: Claude <claude@anthropic.com>
- Remove workflow_dispatch trigger (no artifacts on manual dispatch)
- Add nodejs npm to Ubuntu test dependencies
- Add explicit permissions: contents: read to workflow
- Replace echo|grep with [[ ]] pattern matching (4 instances)
- Drop ambiguous 2>&1 from install commands
- Use (( ++ )) arithmetic style in test helpers
Validate deb, rpm, and appimage packages after build in CI.
Tests verify package metadata, file layout, desktop entries,
icons, launcher scripts, asar contents (frame-fix, cowork,
native stub, tray icons), and --doctor smoke tests.
Runs as a reusable workflow with matrix strategy (one job per
format) between build and release jobs, gating releases on
passing artifact validation.
* fix: disable VM file downloads on Linux to prevent checksum loop (#334)
Patch 4 in patch_cowork_linux() previously copied win32 VM file entries
(rootfs.vhdx, vmlinuz, initrd) with Linux-specific checksums. These
checksums drifted from CDN content, causing an infinite download retry
loop for all Linux users — including bwrap users who don't need VM
files at all.
The root cause: Patch 1 opens the yukonSilver feature gate for Linux,
making the VM download path reachable even on bwrap-only installs. The
triage bot missed this because it analyzed unpatched code.
Fix: inject empty file arrays (linux:{x64:[],arm64:[]}) instead of
copying win32 entries. This is safe because:
- The VM backend is non-functional on Linux (bwrap is the only backend)
- Empty arrays make the download loop a no-op (for...of [] skips)
- [].every() returns true (vacuous truth), reporting "Ready" status
- The linux key must exist to prevent TypeError on files["linux"]["x64"]
Removes ~230 lines of checksum infrastructure from build.sh and CI that
maintained checksums for a non-functional feature.
Fixes#334Closes#329Closes#332
Co-Authored-By: Claude <claude@anthropic.com>
* style: clean up stray blank line and use durable issue reference
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
All bugs are ours to investigate and fix. This project's goal is to
take a working Anthropic product and make it work on Linux. Behavioral
differences between Windows/macOS and our build are gaps in our
patching, not someone else's problem.
- Delete 'upstream' label from repo (removed from 7 issues)
- Replace "check patches before blaming upstream" with "all bugs are
ours to fix"
- Remove upstream from label glossary and suggested labels
- Update all references in agent, workflow, and classification schema
Co-Authored-By: Claude <claude@anthropic.com>
Lessons from #329 where triage fabricated claims about manifest entries
and missed that our own patch was the cause:
- Add "check our patches first" rule: for bugs in patched areas, check
build.sh patches before blaming upstream
- Add "verify before stating" rule: only state facts found in code,
never speculate about code existence
- Add "validate network assumptions" rule: use curl to check URLs
before speculating about CDN failures
- Include CLAUDE.md in investigation prompt for full project context
- Increase investigation budget from $1 to $3 for deeper analysis
Co-Authored-By: Claude <claude@anthropic.com>