703 Commits

Author SHA1 Message Date
Aaddrick
7e77833b11 fix(triage): pull broken-expectation rule up into first-pass classify (#469)
Post-rename verification on #448, #449, and #424 showed the first-pass
classifier now leans `enhancement` on all three, while the doublecheck
correctly reads them as `bug`. The disagreement failsafe defers each
to human review, which is safe but wastes the doublecheck as a
classification-recovery mechanism rather than a verification one.

Root cause: the "broken expectation wins" rule lives only in the
doublecheck prompt. First pass sees `enhancement` framing ("breaks X",
"should support Y") and weights it as an enhancement request. Adding
the rule to the primary classify prompt brings first-pass behavior in
line with doublecheck expectations.

Explicit examples added from the test set (minimize-to-tray, APT pool
regression, CTRL+C) so future calibration drift is easier to notice.

Co-authored-by: Claude <claude@anthropic.com>
2026-04-20 23:14:12 -04:00
Aaddrick
7d083d9163 refactor(triage): rename feature classification to enhancement (#466)
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>
2026-04-20 22:58:33 -04:00
Aaddrick
471c62dde0 chore(codeowners): add @sabiut for testing & release quality (#468)
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>
2026-04-20 22:57:28 -04:00
Aaddrick
d0544d44e8 feat(triage): Phase 3 — Stage 6 adversarial reviewer + duplicate gate (#465)
* 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>
2026-04-20 22:13:45 -04:00
Aaddrick
88df8e8e7e fix(triage): raise 8b comment word cap 150 → 300 (#464)
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>
2026-04-20 21:45:32 -04:00
github-actions[bot]
f2487e0b19 Update Claude Desktop download URLs to version 1.3561.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.0+claude1.3561.0
2026-04-21 01:38:28 +00:00
Aaddrick
caec9182c8 fix(triage): investigate timeout bypasses errexit + bump to 10m (#463)
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>
2026-04-20 21:30:15 -04:00
Aaddrick
ce2137f63a fix(triage): pass investigate schema to claude CLI (#462)
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>
2026-04-20 19:23:28 -04:00
Aaddrick
82908fbe64 fix(triage): harden Investigate step against hangs and parser drift (#461)
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>
2026-04-20 19:08:58 -04:00
Aaddrick
1de897f56e feat(triage): dry_run input + pre-dispatch fixes (#460)
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>
2026-04-20 18:48:01 -04:00
Aaddrick
755bef4c28 Merge pull request #459 from aaddrick/staging/triage-v2
feat(triage): issue triage v2 (Phases 0-2)
2026-04-20 18:37:45 -04:00
Aaddrick
34631068ee feat(triage): Phase 2 — investigate, mechanical validate, 8a findings (#458)
Extends the Phase 1 deferral-only pipeline with the bug-investigation
path: Stages 3 (fetch reference), 4 (investigate), 5 (mechanical
validate), 7 partial (decision gate), and 8a (findings variant).
Non-bug classifications still route through 8b; adversarial reviewer
is Phase 3.

## What Phase 2 adds

- **Stage 3 — Fetch reference.** `gh release download --pattern
  'reference-source.tar.gz'` with 3× exponential backoff (2s/8s/32s).
  Fetch failure routes to 8b with reason `reference-source unavailable`
  (the 7th reason added to `reasons.json`).
- **Stage 4 — Investigate.** `schemas/investigate.json` +
  `prompts/investigate.txt`. Claude reads repo + reference source via
  tool access (`--dangerously-skip-permissions`), emits structured
  findings / pattern_sweep / proposed_anchors / related_issues. Prompt
  enforces hypothesis voice, cross-cutting-sweep obligation, hard
  schema bans.
- **Stage 5 — Mechanical validation.** `.claude/scripts/triage/
  validate.sh` — pure bash. Checks per finding: file exists, line
  range valid, evidence_quote grep-matches at cited line, closed-world
  options extracted for identifier claims (grep heuristic for Phase 2;
  ast-grep upgrade deferred to Phase 3). Per anchor: `grep -P` match
  count exactly equal to expected_match_count. Per related_issue:
  `gh issue view` fetch + body excerpt. Emits `validation.json`.
- **Stage 3a — Version drift check.** Compares classify's
  `claimed_version` against `vars.CLAUDE_DESKTOP_VERSION`. Drift flag
  routes to 8b with `version drift` reason; investigation still runs.
- **Drift-bridge sweep.** `.claude/scripts/triage/drift-bridge.sh` —
  bash, resolves claimed_version to approximate date via `git log
  --grep`, then date-windowed `git log` on finding files + `gh pr
  list` basename search. Candidates attach to 8b as a rendered bullet
  block.
- **Stage 7 partial — Decision gate.** Priority: drift → 8b drift-
  bridge · fetch failure → 8b reference-source-unavailable ·
  investigate failure or zero surviving findings → 8b no-findings ·
  avg confidence < medium → 8b low-confidence · else → 8a.
- **Stage 8a — Findings variant.** `schemas/comment-findings.json` +
  `prompts/comment-findings.txt`. Claude emits structured comment
  object (hypothesis_line, findings[], patch_sketch?, related_issues);
  bash renders markdown. No post-hoc prose stripping — the schema
  guarantees shape. 400-word cap truncates the `<details>` patch block
  only.
- **Stage 8b extension.** Drift-bridge-candidates bullet block renders
  only when reason is `version drift` AND the sweep returned ≥1
  candidate. Phase 1's first-issue privacy note + reason-enum post-
  processor are preserved.
- **Stage 9.** Labels: 8a → `triage: investigated`; 8b routing
  unchanged. Artifacts extended with `investigation.json`,
  `validation.json`, `drift-bridge-candidates.json` (conditional).

## Risks validated locally

- Mechanical validation catches fabricated identifiers *and* non-
  matching anchors — smoke tested with a two-finding / two-anchor
  fixture (one real, one fabricated per kind); failure_reasons fire
  correctly on the fabricated ones.
- Closed-world extraction via grep heuristic: on a JS switch with
  three cases, returns all three as `closed_world_options` bounded
  to ±100 lines.
- `grep -c` exits 1 on no-match and prints "0" — validated the `|| true`
  idiom doesn't double-count.

## Deferred

- Stage 6 adversarial reviewer (Phase 3)
- Confirmed-duplicate routing with Stage 6's exact/related rating
  (Phase 3)
- Feature-design variant 8c (Phase 4)
- Suspicious-input tells + edit-during-triage detection (Phase 4)
- ast-grep upgrade for closed-world extraction (Phase 3)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:09:15 -04:00
Aaddrick
0f55547523 feat(triage): Phase 1 — gate, classify, 8b deferral, label/post/archive (#457)
Turns the Phase 0 skeleton into a live triage pipeline. Every dispatched
issue now gets a structured human-deferral comment and a triage label.
No investigation yet — that's Phase 2.

## Stages landed (per docs/issue-triage/implementation-plan.md §Phase 1)

- **Stage 1 — Gate.** `github-actions[bot]` author skip; manual dispatch
  intentionally bypasses the already-triaged / needs-human checks (those
  only matter on the `opened` trigger, deferred to cutover).
- **Stage 1 — Input snapshot.** `issue.body`, `issue.updated_at`,
  `sha256(issue.body)` captured before any LLM call; archived as
  `input_snapshot.json`. Edit-during-triage comparison lands in Phase 4.
- **Stage 2 — Classify.** `schemas/classify.json` + `prompts/classify.txt`.
  Fields: classification enum, confidence, claimed_version,
  suggested_labels[], duplicate_of, regression_of. Issue body wrapped as
  untrusted data.
- **Stage 2 — Doublecheck.** `schemas/classify-doublecheck-bugfeature.json`
  + `prompts/classify-doublecheck-bugfeature.txt`. Runs conditionally
  when the first pass returns `bug` or `feature`. Fresh context — no
  first-pass output exposed.
- **Stage 7 (partial) — Reason selection.** Two reasons fire in Phase 1:
  `ambiguous` when the doublecheck disagrees, `no-findings` otherwise.
  The other four reasons in `reasons.json` light up in Phases 2–4.
- **Stage 8b — Human-deferral render.** Bash-only template reading
  `reasons.json`. First-issue privacy note appended when the reporter
  has no prior issues on the repo. Post-processor enforces: reason line
  in `reasons.json` enum, comment under 150 words.
- **Stage 9 — Label + post + archive.** Cached `gh label list` at
  workflow start; cardinality-1 slots (triage state, class, priority)
  applied directly; categories filtered through the cache + blocklist.
  Never emits `priority: critical`. Artifacts uploaded with 14-day
  retention: `input_snapshot.json`, `classification.json`,
  `classification-doublecheck.json` (when ran), `comment.md`,
  `issue.json`, `repo-labels.json`.

## Validation

- actionlint + shellcheck clean on inline bash
- Schemas parse as JSON; prompts validated via jq
- Matches Phase 1 exit criteria once dispatched against real issues
  (bug with stack trace → needs-human + no-findings; ambiguous →
  needs-human + ambiguous; no hallucinated labels applied)

## Deferred to Phase 2+

- Investigation (Stage 4), mechanical validation (Stage 5), adversarial
  review (Stage 6)
- Findings variant (8a), feature-design variant (8c)
- Drift-bridge sweep (extends 8b with candidate commits/PRs)
- Confirmed-duplicate routing (needs Stage 5+6)
- Suspicious-input tells and edit-during-triage detection (Phase 4)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:39:37 -04:00
Aaddrick
b354353a36 feat(triage): Phase 0 scaffold for issue triage v2 (#456)
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>
2026-04-20 17:29:17 -04:00
Aaddrick
b308c0ffd2 docs: add issue triage pipeline design (#455)
Adds the issue triage pipeline design under docs/issue-triage/:

- README.md — base pipeline spec
- implementation-plan.md — stage-by-stage plan
- research-trail.md — references that informed the design

Replaces the original single-file docs/issue-triage.md that was
reverted from main in f829d3b. Squash of 28 drafting commits from
the prior docs/triage-pipeline-design branch (backup at
backup/docs-triage-pipeline-design-pre-rebase).

Co-authored-by: Claude <claude@anthropic.com>
2026-04-20 17:13:39 -04:00
Travis
7e33c095da fix(kvm): probe virtiofsd fallback paths in KvmBackend (#447) (#454)
Follow-up to #453: the daemon still spawns virtiofsd via PATH lookup
(`spawnProcess('virtiofsd', ...)`), so on stock Debian/Ubuntu
(`/usr/libexec/virtiofsd`) and Arch/CachyOS/Manjaro
(`/usr/lib/virtiofsd`) the spawn ENOENTs and KvmBackend silently
falls through to virtio-9p — users who opted into
`COWORK_VM_BACKEND=kvm` and installed virtiofsd get 9p performance
without knowing.

Mirror doctor.sh's `_find_virtiofsd` in JS: probe `COWORK_VM_VIRTIOFSD_BIN`
override, then `which`, then the same fallback list. Pass the resolved
absolute path as argv[0] so the spawn bypasses PATH entirely.

Also:
- Add a `spawnFailed` flag the socket-wait loop checks for early exit
  when the async 'error' event fires (e.g. binary removed between
  probe and exec) — prevents a 5s stall before 9p fallback.
- Guard `this.virtiofsdProcess.kill()` against the race where the
  error handler has already zeroed it.
- Rename doctor.sh's test hook `_COWORK_DOCTOR_VFSD_PATHS` →
  `_COWORK_VFSD_PATHS` so doctor and daemon share the same env var
  for lock-step test parity (shipped 24h ago in #453, zero external
  users).

Verified on CachyOS via a node harness covering 8 scenarios:
PATH hit, fallback hit, fallback ordering, total miss, non-executable
rejection, explicit override wins over PATH, override non-executable
→ null, override missing → null (no fall-through).

All 45 BATS tests still pass after the env-var rename.

Not verifiable locally: Ubuntu `/usr/libexec/virtiofsd` hit (needs an
Ubuntu VM with `qemu-system-common`). Logic is symmetric to the Arch
case that is verified.

Co-authored-by: Claude <claude@anthropic.com>
2026-04-20 15:52:53 -05:00
Travis
89582bb8f0 fix: detect virtiofsd at off-PATH install locations (#447) (#453)
* fix: detect virtiofsd at off-PATH install locations (#447)

Ubuntu ships virtiofsd at /usr/libexec/virtiofsd (from qemu-system-common)
and Arch/CachyOS/Manjaro at /usr/lib/virtiofsd. Neither is on the default
$PATH, so doctor.sh's `command -v virtiofsd` always returned a false
negative — users would install the package and still see "virtiofsd: not
found" (reported most recently by @zabka in #445, originally flagged by
@jarrodcolburn).

Adds a _find_virtiofsd helper that searches PATH first, then the known
off-PATH install locations:

  - /usr/libexec/virtiofsd  (Debian/Ubuntu/Fedora/RHEL)
  - /usr/lib/qemu/virtiofsd (legacy Debian)
  - /usr/lib/virtiofsd      (Arch/CachyOS/Manjaro)

Splits virtiofsd out of the KVM tools loop into a dedicated three-branch
check:

  [PASS] virtiofsd: found                                  — on PATH
  [PASS] virtiofsd: found at <path> (not on PATH)          — off-PATH, bwrap default (virtiofsd unused)
  [WARN] virtiofsd: found at <path> but not on PATH        — off-PATH, COWORK_VM_BACKEND=kvm
         (+ info lines about 9p fallback + symlink Fix)
  [INFO]/[WARN] virtiofsd: not found                       — missing (severity ladder unchanged)

The WARN-on-KVM-active branch surfaces that KvmBackend spawns virtiofsd
by PATH name and will silently fall back to virtio-9p (lower performance)
if the binary is only reachable off-PATH — so the user knows a symlink
is needed to actually get virtiofs performance.

Tests: 6 new BATS cases in tests/cowork-bwrap-config.bats exercise the
helper (PATH hit / fallback hit / ordered fallback / total miss /
non-executable skip / default-list regression guard for the Arch path).
All 45 tests pass.

Does not touch cowork-vm-service.js — teaching KvmBackend to probe
these same paths would give Ubuntu KVM users real virtiofs performance
without a symlink, but that's a separate change.

Fixes #447

Co-Authored-By: Claude <claude@anthropic.com>

* style: collapse unnecessary line continuations in virtiofsd check

Simplifier pass — the five backslash-continued `_warn` / `_info`
invocations in the new virtiofsd three-severity block were all under
63 chars after collapsing, well within the project's 80-char
guideline. The continuations were visual noise, not wrap-driven.

Behavior byte-identical. All 45 BATS tests still pass.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-20 15:34:49 -05:00
Aaddrick
f593cedcac Merge pull request #443 from aaddrick/refactor/build-split-for-codeowners
refactor: split build.sh by subsystem + add CODEOWNERS
v2.0.0+claude1.3109.0
2026-04-20 08:19:35 -04:00
aaddrick
d939b0795e refactor: extract --doctor into scripts/doctor.sh
Moves run_doctor and its 9 internal helpers out of launcher-common.sh
(~670 lines) into their own scripts/doctor.sh. launcher-common.sh
now sources doctor.sh via a BASH_SOURCE-relative path, so any consumer
still gets the run_doctor entry point without needing to know about
the split.

Rationale: the testing / release-quality role concerns itself with
--doctor, and giving that subsystem its own file lets CODEOWNERS
scope it independently of the rest of launcher-common (display
detection, cleanup handlers, electron env) which remain in aaddrick's
domain.

Each packaging target now installs doctor.sh alongside launcher-common.sh:

  scripts/packaging/appimage.sh  → /usr/lib/claude-desktop/{launcher-common,doctor}.sh
  scripts/packaging/deb.sh       → /usr/lib/<pkg>/{launcher-common,doctor}.sh
  scripts/packaging/rpm.sh       → /usr/lib/<pkg>/{launcher-common,doctor}.sh
  nix/claude-desktop.nix         → $out/lib/claude-desktop/{launcher-common,doctor}.sh

Pure-move refactor. Function bodies byte-identical to the pre-split
launcher-common.sh content. Verified: `source launcher-common.sh` still
defines all 19 previous functions (9 launcher + 10 doctor); a live
run_doctor invocation produces the same output as before.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-20 08:09:03 -04:00
aaddrick
338f6ec1c1 docs: refresh for scripts/ split layout
Updates agent definitions, learnings, CLAUDE.md, and BUILDING.md so
path references point at the new module files instead of the old
monolithic build.sh.

Agent definitions:
  .claude/agents/issue-triage.md              — table of per-category
    investigation paths now points at scripts/patches/*.sh and
    scripts/packaging/*.sh instead of "build.sh (search patch_X)".
  .claude/agents/electron-linux-specialist.md — patching-functions
    table now includes each function's file location; directory tree
    illustration reflects the new scripts/ layout.

Documentation:
  CLAUDE.md                                   — "Working with Minified
    JavaScript" section points at scripts/patches/*.sh; frame-fix
    injection attributed to scripts/patches/app-asar.sh; the
    version-bump checks now grep scripts/setup/detect-host.sh.
  docs/BUILDING.md                            — automated version
    detection paragraph now mentions scripts/setup/detect-host.sh as
    the file that holds the URLs.
  docs/learnings/cowork-vm-daemon.md          — Patch 6 pointer now
    says scripts/patches/cowork.sh; line-number references dropped in
    favour of anchor-based search (line numbers drift between releases).
  docs/learnings/plugin-install.md            — Key Files section
    points at scripts/patches/cowork.sh for patch_cowork_linux.

Historical changelog-style references (e.g. docs/cowork-linux-handover.md
describing what was "added to build.sh" during initial cowork work)
are intentionally left unchanged — they describe a point-in-time state
of the codebase.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-20 07:31:02 -04:00
aaddrick
01f7125d6a ci: refresh issue-triage prompts for scripts/patches/ layout
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>
2026-04-20 07:27:17 -04:00
aaddrick
564f465840 ci: update check-claude-version paths to scripts/setup/detect-host.sh
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>
2026-04-20 07:26:40 -04:00
aaddrick
526acbad1e ci: enable shellcheck -x to follow sourced modules
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>
2026-04-20 07:25:21 -04:00
aaddrick
d574ac54d7 chore: add .github/CODEOWNERS for per-subsystem review ownership
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>
2026-04-20 07:12:22 -04:00
aaddrick
ff4821e087 refactor: split build.sh into topical modules under scripts/
Splits the 2124-line build.sh into a 318-line orchestrator plus
16 topical modules, grouped so CODEOWNERS can assign per-subsystem
reviewers:

    scripts/_common.sh              shared shell utilities
    scripts/setup/                  host detection, deps, download
    scripts/patches/                regex patches on minified JS
      _common.sh                    extract_electron_variable etc.
      app-asar.sh                   wrapper injection
      titlebar.sh
      tray.sh                       menu handler + icon selection
      quick-window.sh
      claude-code.sh
      cowork.sh                     cowork linux patching (largest)
    scripts/staging/                post-patch file staging

build.sh now sources each module in dependency order and retains
only run_packaging, cleanup_build, print_next_steps, and main.
All globals stay at the top of build.sh and are read by sourced
modules; each module's header documents which globals it reads and
mutates (implicit-contract documentation).

This is a pure-move refactor. Function bodies were copied verbatim
— verified by byte-identical diff of the function set vs the
pre-split build.sh (34 functions, all present with identical bodies).

Note: .github/workflows/shellcheck.yml may benefit from a '-x' flag
so shellcheck follows the new '# shellcheck source=' directives, but
that CI tweak is left as a separate concern.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-20 07:12:22 -04:00
aaddrick
6cd85ff9e4 refactor: relocate packaging scripts into scripts/packaging/
Moves scripts/build-{appimage,deb,rpm}-package.sh into
scripts/packaging/ so CODEOWNERS can scope packaging-format
ownership independently from the build orchestrator. The single
content change per file is the relative-path fix for
launcher-common.sh (which stays in scripts/), updating:

    \$script_dir/launcher-common.sh
    -> \$(dirname "\$script_dir")/launcher-common.sh

so the scripts still find the shared launcher library after moving
one directory deeper.

Part of the build.sh split for CODEOWNERS.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-20 06:55:45 -04:00
github-actions[bot]
2d6a645c76 chore: update flake.lock 2026-04-20 03:16:52 +00:00
aaddrick
f829d3bf5f Revert "docs: add issue triage pipeline design document"
This reverts commit 1d020aa. Moving the change to a branch for review
instead of shipping directly to main.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-19 17:21:58 -04:00
aaddrick
1d020aa628 docs: add issue triage pipeline design document
Captures the designed-from-scratch triage pipeline: seven load-bearing
principles, nine stages (gate, double-checked classify, fetch-reference,
structured investigate, mechanical validation with closed-world
extraction, adversarial review with fresh context, decision gate,
template-enforced comment generation, label/post/archive), the feedback
loop (slash command, 👎 reaction, curated corrections file), and health
monitoring.

References Anthropic's published agent patterns (framework for safe
agents, Code Review product, claude-code-security-review action), LLM
hallucination research, and GitHub's production triage systems for the
patterns the design adopts.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-19 17:15:29 -04:00
Travis
44cd5a6c24 fix: forward userSelectedFolders[0] as sharedCwdPath on cowork spawn (#412) (#436)
* fix: forward userSelectedFolders[0] as sharedCwdPath on cowork spawn (#412)

The cowork-vm-service daemon already honors a `sharedCwdPath` field on
the spawn IPC payload with priority over `cwd` (resolveWorkDir in
scripts/cowork-vm-service.js:500), but the upstream Electron app never
populates it on Linux backends. Every spawn arrives with only
`cwd=/sessions/{name}`, so the daemon derives the host path from
mountMap heuristics (PRs #389/#392/#411 cover the symptoms).

Patch 12 threads the user-selected folder through three sites so the
daemon receives the host path explicitly:

  12a. At `this.getVMSpawnFunction({...})` config assembly, inject
       `sharedCwdPath: SESSION.userSelectedFolders?.[0]` alongside the
       existing mount config.
  12b. At the Kyr() -> VMClient.spawn() call, forward
       `SESSION.sharedCwdPath` as a new 13th positional argument.
  12c. In the spawn() method body, accept a new trailing parameter
       and set it on the IPC payload with a `VAR && (I.sharedCwdPath=VAR)`
       guard matching the existing setter chain.

All three sub-patches detect prior application and no-op on re-run
(idempotent). If any site fails to match on a future upstream, the
daemon-side fallback from #392 keeps cwd resolution working — the
daemon workarounds in #389/#392/#411 remain as safety nets.

Verified against app.asar extracted from Claude-Setup-x64.exe for
version 1.3109.0. All three edits apply, output parses cleanly, a
second run is a no-op.

Co-Authored-By: Claude <claude@anthropic.com>

* refactor: simplify Patch 12 block

Reduce Patch 12 (#412) by 25 lines without changing the patched
output:

- 12a: use extractBlock('{') directly on the getVMSpawnFunction
  argument and splice before its closing '}', removing the
  inverted backward walk over the parenthesised block.
- 12a: replace the exec-until-null loop with matchAll + last
  element to read the session-var name.
- 12c: replace the whole.replace().replace() + code.replace(whole)
  reassembly chain with an index-based splice using
  spawnMatch.index.
- 12c: drop the unneeded .split('') on the letter bag and inline
  newArgList.
- Trim the prologue comment to match the density of Patches 1-10.

Patched index.js is byte-identical against the 1.3109.0 fixture
and the three idempotency log lines still fire on re-run.

Co-Authored-By: Claude <claude@anthropic.com>

* fix: address aaddrick's review — robust 12a anchor, 12b uniqueness assertion

Two fixes from PR #436 review:

1. **12a: drop fragile backscan, route through this.sessions.get().**
   The previous `VAR.userSelectedFolders` backscan returned 10 matches
   across 4 distinct vars (t, se, Ke, We) in the v1.3109.0 window —
   last-match landed on `t` by coincidence, and `We` in particular is
   a for-loop variable one upstream re-order away from becoming the
   new "last". Swap to the canonical accessor the class already uses:
   `this.sessions.get(sessionId)?.userSelectedFolders?.[0]`. The
   sessionId var is extracted from the config's first field
   `{sessionId:VAR` — scoped to the config block, 100% guaranteed
   present, immune to unrelated references leaking in.

2. **12b: matchAll + uniqueness assertion.** The previous code used
   `code.match()` which silently took the first hit if a second
   upstream call site ever appeared. Switch to `matchAll` with
   `length === 1` assertion; WARN-and-skip on anything else so a
   wrong-site forwarding becomes detectable instead of silent.

3. **Drop misleading ordering comment in 12c.** The "12c before 12b
   so property name is fixed" note was wrong — the property name is
   a hardcoded string literal in both sub-patches, so the ordering
   is cosmetic.

Verified: dry-run still applies all three patches on 1.3109.0 source,
output passes `node --check`, the three sharedCwdPath edits are
byte-stable across runs (the non-idempotency in Patch 9 is
pre-existing and orthogonal).

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-19 12:25:29 -04:00
Aaddrick
f19d12c7fb docs: document Anthropic & Partners plugin install flow (#439)
* docs: document Anthropic & Partners plugin install flow (#396)

Captures the non-obvious bits of the plugin install flow that came
out of the #396 / PR #435 investigation:

- Remote renderer architecture (claude.ai in BrowserView) and why
  the main process can't control pluginContext.mode or
  pluginSource.
- Current 1.3109.0 install gate, listing filter, and A0() gating
  points with line citations.
- Backend endpoints, identity headers injected by the app, and
  auth surface.
- Full post-mortem of issue #396: old 1.1.7714 gate vs current
  1.3109.0, why it reproduced in the Directory, and the
  coordinated upstream fix.
- Live investigation recipe: enabling main-process DevTools,
  header-spoofing harness, breakpointing the install gate.
- Tip about using reference-source.tar.gz from releases for
  cross-version source diffing.

Co-Authored-By: Claude <claude@anthropic.com>

* docs: tighten redundant intro in plugin-install learning

Collapse the two near-duplicate sentences in "Why This Exists"
into one. The bold insight already states the renderer is
remote; the follow-up then repeated it.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-19 10:21:37 -04:00
Aaddrick
e92aea149f fix: strip mode on node-pty cp at source, retire chmod (#438)
Follow-up to #432. Instead of chmod'ing read-only files after the fact
in finalize_app_asar(), pass --no-preserve=mode on the cp invocations
in install_node_pty() so Nix-store 0444 bits never propagate into the
staging tree. This makes app.asar.contents internally consistent and
removes the need for the post-hoc chmod.

Also applied to the finalize_app_asar() cp from $pty_release_dir for
consistency, since that read also originates in the Nix store when
--node-pty-dir is set.

npm-install flows are unaffected: --no-preserve=mode forces default
(0666 & ~umask) mode, which matches what npm-installed files already
have.

Co-authored-by: Claude <claude@anthropic.com>
2026-04-19 08:10:22 -04:00
Alexis Williams
50b10ed953 fix: chmod node-pty unpacked files before overwriting in Nix builds (#432)
asar pack --unpack preserves Nix store read-only permissions on .node
files it extracts to app.asar.unpacked/. The subsequent cp -r fails
with 'Permission denied' trying to overwrite those read-only files.

Add chmod -R u+w before the copy to make any existing files writable.
2026-04-19 08:01:15 -04:00
Aaddrick
4cc6cc2183 Update sponsorship section in README.md
Removed sponsorship cost details and duplicate sponsorship link.
2026-04-19 02:34:00 -04:00
Travis
3c843244b3 fix: diagnose AppArmor userns block on bwrap probe (#351) (#434)
* fix: diagnose AppArmor userns block on bwrap probe (#351)

Ubuntu 24.04+ ships apparmor_restrict_unprivileged_userns=1 by
default, which blocks the user namespace bwrap needs to start. The
daemon's probe then fails, auto-detect silently falls through to
KVM, and KVM hangs waiting for a rootfs the user hasn't set up —
leaving Cowork stuck in a retry loop with no clear error.

- Classify the probe failure (classifyBwrapProbeError) so the daemon
  can distinguish AppArmor/userns blocks from generic failures and
  log a pointer to the TROUBLESHOOTING.md remediation.
- Stop falling through to KVM when bwrap is installed but blocked;
  drop to host-direct instead so users see a working (if unsandboxed)
  Cowork and the reason bwrap didn't engage. Users who actually want
  KVM can still set COWORK_VM_BACKEND=kvm.
- Mirror the probe + diagnosis in `--doctor` so misconfigured systems
  get the same actionable output without waiting for a daemon log.
- Document the AppArmor profile workaround in TROUBLESHOOTING.md.
- Credit @hfyeh for the diagnosis and profile snippet.

Co-Authored-By: Claude <claude@anthropic.com>

* refactor: simplify PR #434 per cdd-code-simplifier

Drop redundant `-n` guard around the COWORK_VM_BACKEND case in
`--doctor`: the `${VAR,,}` expansion is already safe on an unset var
(no `set -u` in this script) and the `kvm|host` arms simply don't
match an empty string.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-19 01:12:13 -05:00
Travis
9e577cc3d5 fix: suppress Cowork tab auto-select on every launch (#341) (#433)
* fix: suppress Cowork tab auto-select on every launch (#341)

Patch 4's empty Linux bundle manifest makes `[].every()` return
true vacuously, so `iBA()` reports "VM files present" and
`getDownloadStatus()` returns Ready on every startup. The remote
web app treats a startup observation of Ready as the same
download-completed transition that auto-navigates macOS/Windows
users to Cowork after their first download — Linux users hit it
on every launch.

Add Patch 4b to short-circuit `getDownloadStatus()` to
NotDownloaded on Linux. `iBA()` is left alone so the `download()`
IPC still succeeds instantly and the Cowork tab still works when
clicked — the web app's setup UI just passes through.

Anchor is stable: `getDownloadStatus` and the enum property
names (.Downloading, .Ready, .NotDownloaded) are readable in the
minified bundle. Verified against 1.3109.0 with an isolated
node run; idempotent on re-runs.

Co-Authored-By: Claude <claude@anthropic.com>

* refactor: destructure regex match in Patch 4b

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-19 00:02:58 -05:00
Travis
c4fe361002 fix: home --dir before SDK --ro-bind in bwrap sandbox (#426)
Picks up #388 (filiptrplan) and rebases onto current main without the whitespace-only churn that was blocking merge. Functional change is identical to what was already approved.

bwrap processes mount args in order. When the SDK binary lives under $HOME (e.g. ~/.config/Claude/claude-code-vm/), the --ro-bind of its parent directory was added before --dir $HOME. The later --dir wiped out the bind mount. Moving --dir $HOME first fixes the execvp failure.

Co-Authored-By: Filip Trplan <info@trplan.si>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:00:43 -05:00
Travis
36d08ecca8 fix: only route claude commands through SDK binary in cowork-vm-service (#430)
resolveCommand() was substituting sdkBinaryPath for every command once
installSdk populated it. Since the SDK binary is always literally
`claude` (resolveSdkBinary at line 540 joins the subpath with
'claude'), this meant MCP's `bash -c '...'` spawns actually ran
`claude -c '...'` inside the sandbox, which hit an auth check and
failed with "Not logged in · Please run /login".

Limit the substitution to commands whose basename is `claude`. Shells
and any other binaries now fall through to the existing fs.existsSync
/ `which` path resolution, restoring `mcp__workspace__bash` and every
shell-dependent skill (pptx, xlsx, pdf, hybrid-reader, libreoffice-
rules, pip/npm workflows).

Fixes #427

Co-authored-by: Claude <claude@anthropic.com>
2026-04-18 22:49:43 -05:00
Travis
dca3044407 Merge pull request #410 from RayCharlizard/fix/408-cowork-vm-daemon-recovery
fix: cowork-vm-service daemon recovery and crash diagnostics (#408)
2026-04-18 22:11:12 -05:00
Travis Stockton
87f4f0fca7 Merge main to revalidate Patch 6 against 1.3109.0
Catches up to current upstream URLs (1.3109.0), Joost-Maker's #418
identifier-widening fix in Patch 9, and #421's existsync/node-pty fix.
PR #410's last CI ran on April 16 against 1.2773.0 and showed
'WARNING: Could not find retry delay for auto-launch patch' — this
merge re-runs CI against current main to surface whether Patch 6's
regex anchors still match on 1.3109.0.
2026-04-18 21:16:48 -05:00
Travis
e18a76facf fix: launcher-common.sh self-match and stale socket cleanup (#407) (#425)
* fix: launcher-common.sh self-match and stale socket cleanup (#407)

Three related bugs in scripts/launcher-common.sh that combine to break
Claude Desktop startup after any crash that reparents the cowork daemon
on Debian/Ubuntu/Mint systems.

1. cleanup_orphaned_cowork_daemon — the old pgrep pattern
   'claude-desktop' self-matches the launcher's own bash process
   (cmdline `bash /usr/bin/claude-desktop`), causing the function to
   return early on every invocation. The SIGTERM loop never runs.
   Replaced with `pgrep -f 'app\.asar'` plus $$/$PPID exclusion,
   --type= filter (skips chromium helpers), and /proc/*/status check
   (skips stopped/zombie launcher bashes). Added SIGKILL escalation
   after ~2s so cleanup_stale_cowork_socket reliably sees no daemon.

2. cleanup_stale_cowork_socket — the old implementation required
   socat (not preinstalled on Debian/Ubuntu/Mint) and fell through
   to a find -mmin +1440 check that ignored any socket younger
   than 24h. Rewritten to use the ordering invariant:
   cleanup_orphaned_cowork_daemon runs first and kills any orphan,
   so at this point an extant daemon proves the socket is live and
   an absent daemon proves the socket is stale. No socat dependency.

3. run_doctor orphan check — same self-match flaw as (1).
   claude-desktop --doctor reported [PASS] Cowork daemon: running
   (parent alive) on systems with a genuine orphan, actively
   misleading users trying to diagnose this failure. Applied the
   same detection primitive as (1).

Complements #410 (daemon-side crash recovery): #410 reduces how
often orphans are created; this ensures the launcher actually cleans
them up when they are.

Fixes #407

Co-Authored-By: martin152 <martin152@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: credit martin152 in Acknowledgments for #407 launcher fix

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

* style: quote RHS of $$/$PPID comparisons (SC2053)

Shellcheck SC2053: quote RHS in [[ ]] equality tests to prevent glob
matching. No behavior change — $$ and $PPID are always numeric PIDs.

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

---------

Co-authored-by: martin152 <martin152@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:28:22 -05:00
RayCharlizard
951462363e fix: translate guest paths inside --allowedTools and --disallowedTools (#411)
cleanSpawnArgs only translated --add-dir and --plugin-dir flag pairs.
The Electron app emits permission patterns like
Edit(//sessions/{name}/mnt/.auto-memory/**) and Write(...) inside the
single comma-separated --allowedTools value, and those reached the
spawned `claude` CLI verbatim. Permission rules referencing
non-existent guest paths cannot match the real on-disk locations, so
auto-memory grants silently no-op even after #389 made the underlying
path resolvable and #392 fixed the cwd resolution.

This adds two helpers and wires them into cleanSpawnArgs:

  splitToolList(csv):
    Paren-aware split so "Bash(npm test, npm build)" is one entry
    rather than two. Returns an array of raw entries.

  translateEmbeddedGuestPaths(csv, mountMap):
    Walks each entry. "Tool" is passed through. "Tool(pattern)" is
    translated when the pattern looks like a /sessions/ guest path.
    Defensively normalizes leading "//" (the Electron app emits double
    slashes via path.join('/', '/sessions/...')). Entries whose mount
    cannot be resolved are dropped from the CSV; the flag itself is
    kept (a permission rule that can never match is worse than absent).

cleanSpawnArgs now recognizes --allowedTools and --disallowedTools as
"tool-list flags" alongside the existing single-path flags. Single-path
behavior is unchanged.

BATS coverage in tests/cowork-path-translation.bats covers
splitToolList (paren handling, empty/null), translateEmbeddedGuestPaths
(passthrough, double/single-slash translation, drop-on-miss, host-path
passthrough, mcp__ tool names, empty/null), and the cleanSpawnArgs
integration for both new flag types.

Refs: #245 (umbrella), #389 (memory env translation), #392 (cwd fix).

Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
2026-04-18 18:51:38 -05:00
RayCharlizard
37379b45ac fix: resolve working directory from primary mount on HostBackend (#392)
* fix: resolve working directory from primary mount on HostBackend

The Electron app sends `cwd=/sessions/{name}` (a session-root guest
path) for every Cowork session. `resolveWorkDir()` attempts to
translate this via `translateGuestPath()`, but that function's regex
requires `/sessions/{name}/mnt/{mount}/...` — the session root has no
`/mnt/` component, so translation always fails and CWD falls back to
`os.homedir()`.

BwrapBackend avoids this because it overrides `spawn()` and derives CWD
from the primary user mount (first non-dotfile, non-uploads key in
`mountMap`). HostBackend goes through `resolveWorkDir()` which lacked
this fallback.

Add the same primary-mount derivation to `resolveWorkDir()`: when the
CWD is a session-root guest path that `translateGuestPath()` cannot
resolve, find the primary user mount from `mountMap` and use its host
path. Falls back to homedir only when no user mount exists.

Verified with a Node.js test harness simulating the exact spawn
parameters from live session logs — the fix produces the correct
project directory while all edge cases (no user mount, empty mountMap,
host paths, sharedCwdPath precedence) behave correctly.

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

* test: cover resolveWorkDir primary-mount fallback; extract findPrimaryMount

Adds BATS coverage for the session-root cwd fix and extracts the
primary-mount derivation into a shared findPrimaryMount() helper so
HostBackend's resolveWorkDir() and BwrapBackend.spawn() share one
canonical implementation instead of two copies that can drift.

Tests:
- resolveWorkDir: session-root cwd uses primary user mount
- resolveWorkDir: session-root cwd skips dotfile and uploads mounts
- resolveWorkDir: session-root cwd with no user mount falls back to home
- findPrimaryMount: returns null for null/undefined/empty mountMap
- findPrimaryMount: returns first non-dotfile non-uploads key
- findPrimaryMount: returns null when all mounts are dotfiles or uploads
- findPrimaryMount: insertion order determines primary when multiple exist

The inline test copy of resolveWorkDir is updated to match the new
production logic.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 18:51:10 -05:00
Aaddrick
2fd9faf9db fix: cowork existsSync crash + node-pty asar manifest (#421)
fix: cowork existsSync crash on 1.3109+ and unblock node-pty terminal
v1.3.32+claude1.3109.0
2026-04-17 16:21:47 -04:00
Joost-Maker
3150477f55 fix: mark node-pty native modules as unpacked in asar manifest
`install_node_pty()` copied only `lib/` and `package.json` into
`app.asar.contents/node_modules/node-pty/`, and `finalize_app_asar()`
packed app.asar without `--unpack`. The `.node` binaries were then
separately dropped into `app.asar.unpacked/.../node-pty/build/Release/`.

Result: the asar manifest had no entry for `node-pty/build/` at all.
When node-pty's loader (inside the asar) does
`require('../build/Release/pty.node')` from `lib/utils.js`, Electron's
asar -> .unpacked redirect never fires because the redirect requires
a manifest entry annotated as unpacked. The require returns
MODULE_NOT_FOUND despite the binary existing on disk, and Claude Code
mode shows "Failed to load terminal backend" on every shell session
attempt.

Two-part fix:
1. install_node_pty(): also stage `$pty_src_dir/build/` into
   app.asar.contents so the pack step has the .node files to work
   with.
2. finalize_app_asar(): pass `--unpack '**/*.node'` to `asar pack`
   so the binaries get moved into app.asar.unpacked/ AND recorded
   in the manifest as unpacked.

Verified: the new asar manifest now includes
  UNPACKED: /node_modules/node-pty/build/Release/pty.node
  UNPACKED: /node_modules/node-pty/build/Release/conpty.node
  UNPACKED: /node_modules/node-pty/build/Release/conpty_console_list.node
and Claude Code's terminal loads successfully.

The pre-existing copy-to-.unpacked step in finalize_app_asar() is now
redundant but harmless (writes the same bytes); kept for now to
minimize diff and preserve the --node-pty-dir flow.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-17 14:05:40 +02:00
Joost-Maker
2f6194ff5a fix: capture $-prefixed identifiers when extracting cowork vars
Patch 9 in patch_cowork_linux() extracts six minified variable names
from the win32 block to template a Linux block. The extraction regexes
used `(\w+)` which does not match `$` — JavaScript identifiers can
start with `$`, `_`, or a letter.

Claude >= 1.3109.0 renamed the local fs reference inside startVM's
win32 block from `e` to `$e` (likely to avoid shadowing the function
parameter `e`, which is the options object). The existing regex
`(\w+)\.existsSync\(` scans `$e.existsSync(U)`, skips the `$`, and
captures just `e`. Patch 9 then injects a Linux block calling
`e.existsSync(_ls)` — but `e` resolves at runtime to the options
object, so the call dies with `TypeError: e.existsSync is not a
function` and Cowork never boots on Linux.

Widen all six extraction patterns to `[$\w]+`. Also widen the
adjacent unanchored matchers in `archMatch` for consistency.

Add a defensive strip step before injection: if a future upstream
emits its own `if(process.platform==="linux"){...}` block right after
the win32 close brace, brace-count to its end and remove it so we
don't end up with two competing Linux blocks.

Verified: a clean rebuild now logs
  vars: path=ae fs=$e log=qe stream=SL arch=Bre bundle=r
and the asar's injected block contains `$e.existsSync(_ls)`. Cowork
starts cleanly: `[VM:start] Startup complete, total time: 1242ms`,
the VM agent spawns, and prompts get responses end-to-end.

Fixes #418

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-17 14:04:01 +02:00
github-actions[bot]
20802908a7 Update Claude Desktop download URLs to version 1.3109.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v1.3.31+claude1.3109.0
2026-04-17 01:39:24 +00:00
Travis Stockton
ef2aac500d refactor: simplify cowork daemon recovery patch (#408)
Collapse Patch 6b console.log calls to single lines to match the
convention used in Patches 7-9. Each message fits well under 80
characters and doesn't need to be split across three lines.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-16 16:53:30 -05:00
Travis Stockton
fe403ccce0 docs: add cowork-vm-daemon learnings
Capture the architecture and failure modes of the Linux cowork-vm
daemon — respawn logic, crash diagnosis, and the one-shot-guard /
preserved-image pitfalls that caused issue #408. Intended for
future contributors (human or AI) who need to navigate this area
without re-deriving it from minified JS.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-16 12:06:24 -05:00
Travis Stockton
a349dee057 feat: always-on lifecycle logging for cowork-vm-service (#408)
Previously the daemon was forked with stdio:"ignore" and its
internal log() was gated by COWORK_VM_DEBUG=1, so a mid-session
crash left no trace anywhere. Issue #408 surfaced this: the
daemon died silently after ~40 minutes and the cause was
unrecoverable from logs.

Changes:
- mkdirSync the log directory once at module load so writeLog()
  isn't silently discarded when the daemon is the first thing
  writing under ~/.config/Claude/logs/.
- Add logLifecycle() — an always-on writer (bypasses DEBUG) for
  startup, listening, SIGTERM, SIGINT, uncaughtException,
  unhandledRejection, and process exit. A missing startup entry
  means fork() didn't complete; a startup with no matching exit
  means SIGKILL (OOM killer, kill -9, etc).
- Hook logLifecycle into the entry point and signal handlers.

Works in tandem with Patch 6's stdio redirect: Node-level crash
dumps (pre-handler native assertions, etc.) land in the same log
file via the fd redirection, so the file becomes the single
source of truth for daemon death.

Co-Authored-By: Claude <claude@anthropic.com>
2026-04-16 12:06:18 -05:00