Skip to content

EPIC cas-9508: Factory session friction + build infra (7/11 closed)#3

Merged
pippenz merged 16 commits into
mainfrom
develop
Apr 23, 2026
Merged

EPIC cas-9508: Factory session friction + build infra (7/11 closed)#3
pippenz merged 16 commits into
mainfrom
develop

Conversation

@pippenz
Copy link
Copy Markdown
Owner

@pippenz pippenz commented Apr 23, 2026

Summary

Seven tasks from EPIC cas-9508 (Factory session friction) plus build tooling & docs. Based on a P1 friction report from yesterday's abundant-mines session; addresses the supervisor/worker pain points that added ~40 min of overhead per multi-worker EPIC.

Closed in this batch (7/11):

  • cas-5572action=mine race: worker identity matching widened to {agent_id, agent_name, CAS_AGENT_NAME, CAS_SESSION_ID} with case/trim tolerance. Eliminates the spawn-race kick pattern.
  • cas-4181 — Factory TUI epic hijack: unified init + runtime picker via shared helper; strict-improvement gate on EpicStarted; ghost-epic liveness escape. Compile-time enforced.
  • cas-1a7c — Lease/status divergence: action=release auto-recovers lease-less InProgress tasks with audit note; new action=reset verb for dead-session recovery.
  • cas-3086 — Close flow: persist worker ReviewOutcome envelope on task record at close attempt; supervisor-close forwards persisted envelope without bypass_code_review; epic-close short-circuits when all subtasks carry valid receipts (with P0 defense-in-depth so subtask residuals still block).
  • cas-a9ab — SessionEnd hygiene: manifest written to .cas/logs/factory-session-{date}.log; gc_report MCP verb; git rev-parse --git-common-dir for correct main-worktree resolution inside factory layout.
  • cas-aeec — SessionStart WIP triage: supervisor-only banner surfaces uncommitted state with per-file (last touched by cas-xxxx) attribution; 20-row cap with gc_report overflow.
  • cas-7f57 — Director dup messages: TeamsManager::write_to_inbox dedupes identical (from, text) within 10-min window + prunes read messages >2h. Unread messages preserved regardless of age (supervisor recovery prompts cannot evaporate). All ops under flock(LOCK_EX).

Side deliverables:

  • .cargo/config.toml — linker swapped from lld to mold (typically 2-5× faster link, dominates incremental Rust test loop). Paired with cargo-nextest installed host-wide.
  • docs/requests/ — session observation report + 4 cross-team request docs from Petra Stella Cloud.

Still open under EPIC cas-9508 (for follow-up session): cas-4513 (Claude Code JS-crash detection, 514-line WIP salvaged), cas-d0f9 (supervisor-skill preflight, doc-only), cas-2749 (worker crash recovery), cas-6cb0 (typed LeaseNotFound, P3).

Every closed task has verifier approval receipts in its close reason + ReviewOutcome envelopes persisted on the task record.

Test plan

  • cargo nextest run -p cas-cli green (covers mcp_tools_test, hooks::, session_hygiene, factory suite — all confirmed green at close time on individual worker branches; merge-combined run needs verification)
  • cargo build --release clean
  • Factory TUI smoke test: start session, confirm epic panel tracks active epic (not lex-greatest)
  • Fresh worker spawn + action=mine returns assignment within 1s without coordination kick
  • Supervisor-close on a worker-jailed task forwards persisted ReviewOutcome without bypass_code_review=true

🤖 Generated with Claude Code

pippenz and others added 16 commits April 23, 2026 10:36
Structured 7-item observation log from abundant-mines EPIC run
(supervisor quick-sparrow-64, session 48456bb1). Covers spawn-time
action=mine race, verification-jail cost under outdated binary, orphan
untracked files, stale InProgress leases, director dup messages, and
bypass_code_review UX gap. ~40 min of supervisor friction quantified
for triage.
mold is a drop-in replacement for lld that is typically 2-5× faster on
the link step, which dominates Rust's edit-test loop wall time on
incremental builds. Keeps linker=gcc; only changes -fuse-ld arg.

Requires: mold package installed (apt install mold).
Companion: cargo-nextest installed globally for faster test execution
(cargo install cargo-nextest --locked). No config file change needed
for nextest; use `cargo nextest run` in place of `cargo test`.
Init (`detect_epic_state`) and runtime (`DirectorEventDetector::detect_changes`)
used different tiebreakers when multiple Open-with-branch epics existed:
init picked the one with most active subtasks; runtime picked the
lexicographically greatest ID. A stray later-ID Open-with-branch epic
appearing mid-session would fire `EpicStarted` and overwrite `epic_state`,
which then scoped `filter_director_agents_to_current_session` to the wrong
epic and blanked the Tasks panel.

- Extract the shared picker `pick_best_open_branch_epic` + score helper
  `open_branch_epic_score` in `director::events` and call it from both paths.
- Change `detect_changes` signature to take `current_epic_id: Option<&str>`
  and gate Open-with-branch `EpicStarted` on strict improvement: a fresh
  zero-subtask epic cannot hijack an already-tracked active epic.
- Two regression tests in app::tests cover the hijack scenario and the
  init-equivalent runtime pick.

cas-4181
Review-time follow-ups for cas-4181:
- Strict-improvement gate now treats `current_epic_id` as `None` if the
  tracked epic is no longer present in `data.epic_tasks` (closed/deleted
  between refreshes). Prevents the TUI from freezing on a ghost id when
  a legitimate new Open-with-branch epic appears.
- Regression test added.
- Fix stale `detect_changes_for_epic` name in picker doc comment.

cas-4181
Session-end hygiene work in flight when worker harness died.
Modified: handlers.rs, handlers_session.rs, factory_ops.rs,
cas-supervisor-checklist builtin. New: session_hygiene.rs.
Not verified — downstream worker should review before building on.

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

Review-time follow-ups for cas-a9ab:
- `main_worktree_path` now asks git for `--git-common-dir` when cas_root
  sits inside a linked worktree. Without this, the factory layout
  `<repo>/.cas/worktrees/<name>/.cas` caused the manifest to label the
  worker's own WIP as "main worktree", inverting the supervisor triage
  promise. Falls back to `cas_root.parent()` if git is unavailable.
- Added four tests (all green):
  - `porcelain_entry_label_covers_documented_codes` pins the six label
    arms + documents current fall-through for rename/unmerged.
  - `manifest_is_append_only_across_multiple_sessions_same_day` guards
    the multi-session breadcrumb contract.
  - `manifest_records_clean_tree_marker` asserts the `(clean)` line.
  - `main_worktree_path_resolves_to_main_repo_from_linked_worktree`
    covers the factory worktree layout regression.

cas-a9ab
… (cas-aeec)

Supervisor SessionStart now prepends a "⚠ Prior-factory WIP detected" banner
to the hook context when the main worktree is dirty. Each entry carries
per-file attribution derived from `git log -1` of the last commit that
touched the file — the first `cas-xxxx` task id in that commit's
subject+body is surfaced alongside the porcelain status. Untracked files
render as "unattributed — no git history"; commits without a task id
render as "no task id in last commit". The banner never mutates anything.

New module pieces in session_hygiene:
- `extract_task_id`: canonical 4-hex cas-id parser (case-insensitive,
  rejects non-hex, over-long runs, and adjacent-alnum non-boundaries).
- `attribute_file_to_task`: `git log -1` wrapper returning the first
  cas-id from the latest commit touching a path.
- `build_session_start_wip_banner`: composes the full banner or returns
  None when clean / git unavailable / cas_root unresolvable.

SessionStart wires the banner into supervisor-role context (prepended so
it hits the preview window). Non-supervisor sessions and clean trees
fall through silently. cas-supervisor-checklist step 6 updated to
document the banner and point at gc_report for on-demand re-run.

Tests (3 new, 11/11 session_hygiene green; 210/210 hooks:: green):
- extract_task_id_accepts_canonical_and_rejects_garbage
- attribute_file_to_task_finds_cas_id_in_last_commit
- build_session_start_wip_banner_renders_attribution_and_skips_clean

cas-aeec
…s-aeec)

Review-time follow-ups for cas-aeec:

- P1 adversarial: Unbounded git log fan-out. Each attributed row spawns
  `git log -1`; a pathological dirty tree (hundreds of files after a
  prior-session crash) turned SessionStart into a multi-second stall
  that the user reads as "Claude Code hanging on startup". Cap banner
  at 20 inline rows via WIP_BANNER_MAX_ENTRIES; overflow prints
  "... and N more — run gc_report for the full list".

- P2 adversarial: Banner prepended above codemap / project-overview,
  displacing them from the SessionStart preview top slot the adjacent
  comments explicitly engineered those modules to land in. Append
  instead (project-overview already does the same for the same reason).

- P3 correctness: Widen label column from {:10} to {:15} so
  "modified-staged" (15 chars) no longer overflows and misaligns the
  attribution column.

One regression test added (build_session_start_wip_banner_caps_rows_on_large_wip_trees)
plus adjusted inner-loop width. 12/12 session_hygiene green.

cas-aeec
Dropped by cloud team 2026-04-23. Two features (cloud sync pull
team-memories for active project; make team config global with
per-project override) + two bugs (push client must detect server-side
skipped rows — complement to cas-0bdc; cloud client 404 on session
start follow-up against existing cas-4244).

Task IDs referenced by cloud team (cas-e38e, cas-eb38) do not yet
exist in this DB — need to be created as cross-team tracking tasks
in a future session. cas-f645 is already open (EPIC cas-2eb3);
cas-4244 was closed via feature-flag workaround, re-filed doc asks
for real route fix.
Partial director dup-message work in progress when session ended.
Modified: cas-cli/src/ui/factory/daemon/runtime/teams.rs.
Not verified — downstream worker should review before building on.

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

Review-time follow-ups for cas-7f57:

- Adversarial P1: retention was age-only, so an unread supervisor
  recovery message to a wedged worker would silently evaporate after
  2h on any subsequent inbox write. `retain` now preserves
  `read: false` messages regardless of age; only already-delivered
  messages are subject to the 2h sweep.
- Efficiency (maintainability P3 / adversarial P3): the dedup
  early-return used to re-serialize + write the inbox file even when
  retention had pruned nothing, producing a write storm under hot
  duplicate senders. Track `retention_pruned` and only persist on
  the skip path when something was actually removed.
- Two regression tests added:
  - write_to_inbox_does_not_dedup_past_window (15-min-old duplicate
    passes through; guards dedup-cutoff sign flip)
  - write_to_inbox_retention_preserves_unread_messages (3h-old
    unread supervisor recovery message survives the sweep; 3h-old
    read nag is still pruned)
- Existing retention test adjusted (stale message marked read=true
  to match the post-fix policy).

13/13 teams tests green.

cas-7f57
… (cas-5572)

Factory workers assigned by supervisor via `task update assignee=<worker-name>`
were reporting "No open tasks" on their first `action=mine` poll, even though
`task show` confirmed the write. Root cause: `cas_tasks_mine` matched assignee
strictly against `agent_id` or the agent-store row's `name` — at spawn time the
row may still carry a stale default name, so the filter missed name-based
assignments.

Widen the filter to consider a de-duplicated identity set built from
`agent_id`, the agent-store name, `CAS_AGENT_NAME`, and `CAS_SESSION_ID`
(the two env vars populated by the factory PTY spawner). Compare
case-insensitive on trimmed values so minor drift (whitespace, casing)
doesn't hide assignments either.

Adds two regression tests in mcp_tools_test/task_tools that reproduce the
reported scenario and lock in the trimmed / case-insensitive behavior.

EPIC: cas-9508 — Factory session friction

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

When a factory worker session dies mid-task, the lease disappears from the
agent store but the task row stays InProgress with the dead worker's
assignee. `action=release` used to error with "No active lease found" and
leave the status divergence in place, forcing operators to manually run
`update status=open assignee=…` to recover.

Two fixes for EPIC cas-9508 factory-session friction:

1. `cas_task_release` now detects the lease-less, non-Open, non-Closed case
   and auto-recovers: transitions to Open, clears assignee, writes an audit
   note to task.notes, and returns a success message flagging the recovery.
   The real "nothing to release on an Open task" path still errors.

2. New `action=reset` verb — an atomic "revive this task from a dead session"
   operation. Force-releases any active lease on the task (no ownership
   check), clears assignee, forces status=Open, writes an audit note.
   Refuses Closed tasks (tells the caller to use `reopen` instead).

Also adds a regression test for AC3 confirming `action=show` immediately
after `action=update` reflects the new status — no read-after-write
snapshot lag in the task-store path.

5 new tests; full mcp_tools_test suite passes (132/132).

EPIC: cas-9508 — Factory session friction

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four new close_ops unit tests:
- persisted_envelope_satisfies_gate_when_req_missing
- persisted_envelope_with_p0_still_blocks
- request_envelope_takes_precedence_over_persisted
- persisted_malformed_envelope_is_rejected

Plus a full-stack integration test in verification_flow.rs
(test_close_forwards_persisted_review_envelope_after_jail) that walks
the cas-3086 scenario end-to-end: worker close with clean envelope hits
verification jail, envelope is persisted, supervisor close without
bypass_code_review=true succeeds by forwarding the persisted receipt.
Verifier feedback: epic_subtask_receipts_cover accepted any structurally
valid envelope, so a subtask with a P0 residual receipt (reachable via
direct DB write or a leak past the subtask close gate) would have let
the epic-close skip the multi-persona gate. Defense-in-depth: require
evaluate_gate(residual) == Allow for every subtask envelope before
treating the union diff as pre-reviewed.
Includes: cas-4181 (TUI epic hijack fix), cas-a9ab (SessionEnd hygiene
+ gc_report), cas-aeec (SessionStart WIP triage banner), cas-7f57
(director dedup + retention).
@pippenz pippenz merged commit f8f1381 into main Apr 23, 2026
pippenz added a commit that referenced this pull request May 12, 2026
P2 #4 — show_help was missing from alt_screen_scroll_input guard. When
the help overlay is open and the focused pane is in alt-screen, wheel
events were incorrectly forwarded to the inner PTY. Fix: add
`|| self.show_help` to the suppression guard and update the doc comment
to enumerate all overlay conditions.

P2 #5 — PgUp (ESC [ 5 ~) and PgDn (ESC [ 6 ~) were not forwarded in
alt-screen mode; the generic ESC handler only consumed the first byte
(bare 0x1b) and left the remaining [5~ as stray ASCII. Fix: add a
dedicated 4-byte sequence check in client_input.rs before the arrow-key
check. In alt-screen mode, the full native sequence is forwarded via
mux.send_input; outside alt-screen, handle_scroll_up/down is called
(same behavior as mouse wheel). Satisfies AC #1 ("same on PgUp").

Added FactoryApp::for_test() — #[cfg(test)] minimal constructor that
builds a FactoryApp without spawning PTYs or opening files, enabling
unit tests for guard logic at the FactoryApp layer.

Tests added:
- sidecar_and_selection::tests (11 new unit tests):
  - Baseline: returns Some when guard clear + pane in alt-screen
  - show_help guard blocks forwarding (P2 #4)
  - MC + mc_focus==None guard blocks forwarding (P1 #1 regression)
  - MC + mc_focus==Workers guard blocks forwarding
  - PgUp/PgDn dispatch fires when alt-screen active (P2 #5)
  - PgUp/PgDn fall through to handle_scroll when NOT in alt-screen
  - Wheel scroll no-regress when not in alt-screen (AC #3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
Per supervisor amendment to cas-ec8f AC: fix the pre-existing test
failure surfaced by my discovery note. The fix turned out to be two
loops (not one), both stale from the v2.13.0 supervisor-default flip:

- Loop 1 (cas-worker.md markers, line 952): previously asserted both
  "cas-code-review" and "close-gate.md". Commit 8b82273 / cas-8962
  ("remove pre-close /cas-code-review step from worker skill")
  deliberately removed the cas-code-review mention from cas-worker.md
  when owner=supervisor became the default. Drop the now-stale
  "cas-code-review" assertion; keep "close-gate.md" — that's still the
  correct pointer from SKILL.md to the gate doc.

- Loop 2 (close-gate.md content markers, line 959): previously asserted
  five inline-block markers — "Close-time Code Review Gate" (old
  section title), "If close is blocked on P0" (legacy P0 hard-block),
  "bypass_code_review" (legacy worker bypass), plus "cas-code-review"
  and "code-reviewer". Commit 167c57e ("docs(skills): finish cas-5815
  supervisor-default flip — purge stale worker-runs-review prompts")
  rewrote close-gate.md to encode the supervisor-owned contract — the
  inline-block markers no longer apply. Replace with three markers
  that encode the *current* contract: "Close Gate" (section title),
  "cas-code-review" (skill still mentioned, but in a 'don't invoke
  pre-close' caveat), and the literal `owner = "supervisor"` flag.

Both inline comments now record the historical context so the next
person who deletes a marker from these docs is forced to notice the
test pin and re-establish a meaningful contract instead of slipping
into another silent-failure state.

Verified: `cargo test test_cas_worker_skill_documents_code_review_gate`
passes (was failing on main since 8b82273); the two new cas-ec8f
regression tests still pass; cas-f645's push_skipped integration tests
still pass.

This is commit #3 on factory/daring-swan-93 (dc95458 cas-f645 + 16f5ba8
cas-ec8f Round 1 + this), per supervisor instruction not to rebase the
prior two.
pippenz added a commit that referenced this pull request May 13, 2026
…screen

cas-72c3 was the deferred test rider for cas-d5fa, blocked on cas-11b0
(FactoryApp::for_test constructor). Per cas-11b0's close note, the
for_test() constructor + 8 unit tests at the FactoryApp layer already
shipped in 53a7bf4 (same day cas-d5fa landed), covering:

  - AC #1 (MC + mc_focus==None + alt-screen → no forward):
      scroll_blocked_by_mc_focus_none
  - AC #2 (show_help + alt-screen → no forward):
      scroll_blocked_by_show_help
  - AC #4 (non-alt-screen no-regress):
      wheel_scroll_no_regress_when_not_in_alt_screen

The genuinely uncovered slice was AC #3 — the daemon-dispatch path in
`client_input.rs` lines 157-187 that maps `MouseScrollUp` to either
`diff_scroll_up()`, `mux.send_input(SCROLL_UP_ARROWS)`, or a no-op,
depending on `show_changes_dialog` and `handle_scroll_up()`'s return.
That branch lives inside a deep `tokio::select!` and can't be exercised
directly from a unit test without spinning up the full client loop, so
this commit pins the three contracts the daemon relies on instead:

1. scroll_arrow_consts_have_exact_byte_shape_cas_72c3
   Asserts SCROLL_UP_ARROWS == `ESC[A` × SCROLL_LINES and
   SCROLL_DOWN_ARROWS == `ESC[B` × SCROLL_LINES. Pre-cas-72c3 the
   only structural assertion (line ~37) checked length, not bytes; a
   typo could silently break wheel forwarding without firing any
   guard.

2. scroll_changes_dialog_blocks_alt_screen_forwarding_cas_72c3
   With both show_changes_dialog AND alt-screen active, `handle_scroll_up`
   must return `Done` (not `AltScreen`). That's the post-condition the
   daemon's outer `if self.app.show_changes_dialog` branch relies on
   to early-return without forwarding arrows to the PTY. Pins both
   up and down directions.

3. daemon_dispatch_table_for_mouse_scroll_up_cas_72c3
   Table-driven mirror of the daemon's three-way decision (`diff`,
   `alt`, `noop`), asserted against five FactoryApp state shapes. Any
   future drift in either the daemon dispatch or `handle_scroll_up`'s
   return contract fails the test row that no longer maps correctly.

All 11 sidecar_and_selection tests (8 pre-existing + 3 new) pass.
Test-only diff; no production code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
EPIC cas-ffc4 child task cas-53d5 closes hypothesis #2 from the
original cloud-team bug doc: `CloudSyncer::pull_team` now keys its
`since=` watermark by `(team_id, project_canonical_id)` instead of
by `team_id` alone. This prevents a user working on team T across
two projects P1 and P2 from seeing their second-project pull
silently skip historical backfill (same "0 of everything" symptom
cas-6ec7 fixed at the endpoint-routing level).

`pull_team` signature takes `project_id: &str` as an explicit
parameter; `cas cloud pull --full` now scopes its watermark clear
to the current (team, project) pair.

EPIC cas-ffc4 remains OPEN — sibling child task cas-1ced (eager
slug resolution at `cas cloud team set` to close hypothesis #3)
will ship as a follow-on patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
Before this fix, `cas cloud team set <uuid>` printed "Slug resolution
deferred — see `cas cloud team show`" and did NOT eagerly resolve the
project canonical id. A new team member who cloned the repo into a
directory whose basename didn't match the canonical slug (daniel.l: clone
named `cas` vs canonical `cas-src`) had their first `cas cloud sync` go
out with `project_id=cas` — silently routing push/pull to a phantom
project. Manual workaround was a directory rename. EPIC cas-ffc4
hypothesis #3, the last lurking onboarding regression.

Changes:

- `config::ProjectConfig`: new struct with `canonical_id: Option<String>`,
  surfaced as `[project]` in `.cas/config.toml`. Wired into the main
  `Config` struct + `merge_missing` so it round-trips through the existing
  TOML loader without touching any sync paths.

- `cloud::config::resolve_canonical_id`: prepended a config.toml read to
  the resolution chain (config.toml → folder-name → path-hash). Fully
  backward-compatible — when no [project] block exists, the chain is
  exactly the prior behavior. New helper `canonical_id_from_config_toml`
  is the read side; `set_canonical_id_in_config_toml` is the write side
  with read-modify-write semantics that preserve other sections (verified
  by `config_toml_preserves_other_sections`).

- `cloud::config::derive_canonical_id_from_git_remote` +
  `normalize_git_remote_url`: derive from `git -C <root> remote get-url
  origin`, normalize to `<host>/<owner>/<repo>` form. Handles HTTPS, HTTP,
  SSH `git@host:owner/repo`, and `ssh://git@host/owner/repo` shapes;
  strips `.git` suffix; rejects unrecognizable shapes (local paths, etc.)
  so the caller falls through to the next resolution step.

- `cli/cloud.rs::execute_team_set`: after persisting team_id, runs the
  resolution flow (config.toml → git remote → defer). Source of
  resolution surfaced to both human and JSON output. The "deferred"
  message survives ONLY when neither config nor git yields a slug —
  explicitly does NOT default to the working-dir basename (the bug this
  task fixes). Signature now takes `cas_root: &Path` for test isolation.

- New `cas cloud project set <canonical-id>` subcommand: manual override
  path for monorepo / custom-layout / non-git directories where
  auto-derivation fails. Writes `[project] canonical_id` to config.toml.

- `cli/cloud.rs::execute_team_show`: now displays the resolved project
  slug alongside team UUID + team slug. JSON output adds `canonical_id`
  key.

- `execute_team_show_for_test` helper exported for integration tests
  (returns the JSON-string payload directly so tests don't need stdout
  capture).

Tests:
- 6 new integration tests in `team_set_slug_resolution_test.rs`:
  config.toml preserve / HTTPS-derive / SSH-derive / no-default-to-basename
  negative / project set writes / team show displays.
- 11 new unit tests in `cloud::config::tests`: URL normalization shape
  table (https/http/ssh/ssh-url/subgroup/local-rejected/empty-rejected),
  config.toml round-trip, preserves-other-sections, resolution-order
  precedence.

29 cloud-sync integration tests + 32 cloud::config unit tests + full
crate suite all green. Build clean. Onboarding-only surface — no
production sync path touched; the prepend to `resolve_canonical_id` is
fully backward-compatible when no [project] block exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
EPIC cas-ffc4 child task cas-1ced closes hypothesis #3 from the
original cloud-team bug doc: `cas cloud team set <uuid>` previously
deferred slug resolution entirely, causing the first sync to go out
with the wrong `project_id` when the working-directory name didn't
match the canonical slug (daniel.l's `cas` vs canonical `cas-src`).
Manual directory-rename was the workaround.

Fix:
- New `[project]` config block (`ProjectConfig { canonical_id }`)
  wired through Config + merge_missing + init.rs literal.
- `resolve_canonical_id` chain becomes config.toml → folder-name →
  path-hash. Backward-compatible.
- New helpers in `cloud::config`: `canonical_id_from_config_toml`,
  `set_canonical_id_in_config_toml` (read-modify-write preserves
  other sections), `derive_canonical_id_from_git_remote`,
  `normalize_git_remote_url` (HTTPS / HTTP / `git@host:owner/repo` /
  `ssh://git@host/...` / `.git` suffix strip).
- `execute_team_set` runs eager resolution after team_id persists;
  output surfaces source (`from .cas/config.toml` / `derived from
  git remote` / deferred). Explicitly does NOT default to working-
  dir basename — that's the exact bug guard.
- New `cas cloud project set <canonical-id>` subcommand for manual
  override (monorepo / non-git / custom layout).
- `cas cloud team show` displays the resolved project slug.

Tests: 6 new integration tests in `team_set_slug_resolution_test.rs`
(preserve config / HTTPS derive / SSH derive / no-basename-default
negative / project set / team show) + 11 unit tests in
`cloud::config::tests` (URL normalization shape table + config.toml
round-trip + section-preserve + resolution precedence).

This is the last task in EPIC cas-ffc4 — the EPIC closes on merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
…loses

EPIC cas-ffc4 closes end-to-end. Last child task cas-1ced closes
hypothesis #3 from the cloud-team bug doc: `cas cloud team set
<uuid>` now eagerly resolves the canonical project slug via
.cas/config.toml → git remote → defer (NEVER falls back to working-
dir basename). New `cas cloud project set <canonical-id>` subcommand
for manual override. `cas cloud team show` displays the resolved
slug. This was the last UX paper-cut from daniel.l's original bug
report — the directory-rename workaround is no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pippenz added a commit that referenced this pull request May 13, 2026
…c4 closes

Closes the original cloud-team P1 end-to-end. EPIC cas-ffc4 had three
child tasks mapping to the three hypotheses from the bug doc:

- cas-6ec7 (v2.15.1): hypothesis #1 — missing endpoint wire-up in
  `cas cloud sync` / `cas cloud pull`
- cas-53d5 (v2.15.2): hypothesis #2 — cross-project watermark reuse
  in `CloudSyncer::pull_team`
- cas-1ced (v2.15.3, this release): hypothesis #3 — deferred slug
  resolution causing wrong `project_id` on first sync after
  `cas cloud team set`

`cas cloud team set <uuid>` now eagerly resolves the canonical
project slug from `.cas/config.toml` then git remote, surfaces the
source, and explicitly does NOT default to the working-dir basename
(that was the bug). New `cas cloud project set <canonical-id>`
subcommand for manual override. `cas cloud team show` displays the
resolved slug.

Bug docs moved to docs/requests/completed/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant