Skip to content

feat(F160): Cat Journey RPG — collaborative nurturing system - #1

Open
bouillipx wants to merge 1 commit into
developfrom
feat/F160-cat-journey-rpg
Open

feat(F160): Cat Journey RPG — collaborative nurturing system#1
bouillipx wants to merge 1 commit into
developfrom
feat/F160-cat-journey-rpg

Conversation

@bouillipx

Copy link
Copy Markdown

Summary

  • Adds the Cat Journey (猫猫足迹) feature: a collaborative nurturing system that records real collaboration events as "footfall" across six growth dimensions
  • Implements ActivityEventBus typed event spine with 4 projectors: JourneyProjector (cat growth), LeadershipProjector (co-creator dimensions), MemoryProjector (evidence crystallization), OtelBridgeProjector (observability bridge)
  • 104 tests / 37 suites / 0 failures covering all projectors and core services

Architecture

F153 OTel ← OtelBridgeProjector
                ↑
ActivityEventBus → JourneyProjector → GrowthService (Redis)
                → LeadershipProjector → LeadershipService (Redis)
                → MemoryProjector → EvidenceStore (SQLite)

Features

  • Six-dimension growth radar (architecture, review, aesthetics, execution, collaboration, insight)
  • Level formula: level = floor(sqrt(footfall / 100)) — quadratic curve
  • Title/achievement unlock with rarity tiers (common → legendary)
  • Bond system tracking cat-to-cat interaction depth (acquaintance → partner → soulmate)
  • Co-creator leadership profile with 6 live + 2 shadow dimensions
  • Monthly review scheduler for growth reports
  • PNG export for journey cards
  • Full web UI: radar charts, detail modals, skill tree, evolution timeline

Phases

  • Phase A (Core Engine): ✅ Complete
  • Phase B (Title + Bond): ✅ Complete
  • Phase C (Moments): ✅ Complete
  • Phase D (Leadership): ✅ 6/7 (D7 calibration needs 2-3 weeks natural data)
  • Phase E (Advanced): 3/7 (E4-E7 blocked on upstream F128/F153)

Test plan

  • node --test packages/api/test/journey-projector.test.js — 35 tests
  • node --test packages/api/test/leadership-projector.test.js — 21 tests
  • node --test packages/api/test/memory-projector.test.js — 21 tests
  • node --test packages/api/test/otel-bridge-projector.test.js — 13 tests
  • node --test packages/api/test/growth-service.test.js — 14 tests

Issue: zts212653#480

🤖 Generated with Claude Code

Adds the Cat Journey (猫猫足迹) feature: a collaborative nurturing system
that records real collaboration events as "footfall" across six dimensions
(architecture, review, aesthetics, execution, collaboration, insight).

Architecture:
- ActivityEventBus: typed in-process event spine (17 event types)
- JourneyProjector: event→footfall mapping for cat growth profiles
- LeadershipProjector: co-creator leadership dimensions (6 live + 2 shadow)
- MemoryProjector: three-tier promotion rules for memory crystallization
- OtelBridgeProjector: bridges product facts → F153 OTel instruments

Features:
- Six-dimension growth radar with level formula: level = floor(sqrt(footfall/100))
- Title/achievement unlock system with rarity tiers
- Bond system tracking cat-to-cat interaction depth
- Co-creator leadership profile with contribution style detection
- Monthly review scheduler for growth reports
- PNG export for journey cards

Tests: 104 tests / 37 suites / 0 failures covering all projectors and services.

Issue: zts212653#480

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bouillipx
bouillipx requested a review from zts212653 as a code owner April 30, 2026 03:47
bouillipx pushed a commit that referenced this pull request May 25, 2026
…ed wiring (zts212653#695)

* fix(embedding): batch splitting + reprobe + collection embed deps

Bug #1: Extract embedIndexedItems from IndexBuilder into shared
embed-utils.ts with 64-item batch splitting. IndexBuilder's private
version sent all items at once — embed-api MAX_BATCH_SIZE=64 caused
>64 items to fail with HTTP 400, silently swallowed by fail-open.

Bug #2: Add reprobeIfNeeded() to EmbeddingService. load() probes
/health once at startup; if embed-api starts later, isReady() stays
false forever. reprobeIfNeeded() re-probes when not ready.

Bug #6: Wire embedDeps into CollectionIndexBuilder and library.ts
rebuild route. Collection rebuild now produces vector embeddings
(was FTS-only). Also pass force param from request body (was ignored).

Upstream: zts212653#693

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[宪宪/Opus-46🐾]

* fix(embedding): wire production call sites for reprobeIfNeeded and per-collection VectorStore

P1 fixes from gpt55 review:
- embed-utils.ts: call reprobeIfNeeded() before isReady() check so all
  callers (IndexBuilder + CollectionIndexBuilder) get late-start recovery
- library.ts: accept embeddingService instead of static embedDeps;
  dynamically load sqlite-vec and create per-collection VectorStore at
  rebuild time (prevents cross-DB vector pollution)
- index.ts: pass memoryServices.embeddingService to libraryRoutes

Tests: add reprobe recovery test, update all embedding mocks with
reprobeIfNeeded method. 68/68 embedding tests pass.

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

* fix(embedding): wire setEmbedDeps on collection store for search-time hybrid

P1 from gpt55 R2: rebuild writes vectors via CollectionIndexBuilder but
the collection's SqliteEvidenceStore never gets setEmbedDeps, so search()
stays lexical-only. Now the rebuild route calls store.setEmbedDeps() after
constructing the per-collection VectorStore, using the system embedMode.

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

---------

Co-authored-by: tianyiliang <tianyiliang@tianyiliangdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.com>
@bouillipx
bouillipx changed the base branch from main to develop July 7, 2026 15:07
zts212653 pushed a commit that referenced this pull request Jul 14, 2026
zts212653#1146)

* fix(invocation): classify interrupted sessions for self-heal + add QueueProcessor terminal write backstop (zts212653#1145)

Two reliability fixes for invocation availability:

1. classifyResumeFailure() now matches "session interrupted" / "already
   interrupted" / "session terminated" as 'missing_session', triggering
   session self-heal (drop stale sessionId + retry fresh). Previously,
   our stall auto-kill (SIGTERM after 7 min idle-silent) caused the CLI
   daemon to mark sessions "interrupted", but the classifier returned
   null → no self-heal → cascading resume failures on every subsequent
   invocation until manual intervention or session TTL expiry.

2. QueueProcessor.executeEntry() finally block now calls
   ensureTerminalStatus() with RouteChainCompletionTracker, matching
   messages.ts F194 Z3 parity. If the catch block fails to update the
   record to 'failed' (e.g. Redis blip), the CAS-guarded backstop
   ensures the invocation reaches a terminal status instead of staying
   'running' and becoming a zombie after 600s grace. The previously
   silent `catch { /* ignore */ }` now logs a warning.

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

* fix(invocation): raise stall auto-kill from 7 to 15 min (zts212653#1145)

7 minutes was too aggressive for LLM inference — when CLI waits for an
API response with large context, it appears idle-silent (no CPU growth,
no NDJSON output) even though a legitimate network request is in flight.
This caused false stall kills, which interrupted active invocations.

15 min gives ample room for long inference while still catching truly
stuck CLIs well before CLI_TIMEOUT_MS (30 min). F194 zombie detection
won't false-fire during this window because InvocationTracker slot is
held by the live generator.

Root cause of: codex sessions getting "interrupted" mid-work.

[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: update stall threshold assertion to match 15-min change (zts212653#1145)

Existing test hardcoded 7*60_000 (420000ms). Updated to match the new
15*60_000 (900000ms) threshold from the prior commit.

[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(invocation): align stall auto-kill to CLI_TIMEOUT_MS (30 min) (zts212653#1145)

The ProcessLivenessProbe cannot distinguish "CLI waiting for LLM API
response" from "CLI truly stuck" — both present as idle-silent (no CPU
growth, no NDJSON output). Any threshold shorter than CLI_TIMEOUT_MS
causes false kills during normal inference.

Evidence: thread_mrj033qnuamv9ghe (sol) and thread_mriwjlo1955spve4
(codex) both show repeated "CLI 响应超时 (420s)" kills during active
tool-call → API-wait cycles. Codex review invocation was killed after
only 16s of work + 7m of waiting for terra API response.

Align to 30 min so CLI_TIMEOUT_MS is the single binding idle constraint.

[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(invocation): address sol review P1/P2/P3 — dynamic stall config, tracker leak, negative tests (zts212653#1145)

P1: Replace hardcoded 30-min constant with buildStallAutoKillConfig()
that tracks resolved CLI_TIMEOUT_MS. When CLI_TIMEOUT_MS=0 (disabled),
stallAutoKill is turned off entirely. Custom values (e.g. 60 min) are
respected instead of being capped at 30 min.

P2: Move chainTracker.release() out of the .get() guard in
QueueProcessor.ts so it always runs — prevents Map leak when
InvocationRecordStoreLike lacks .get().

P3: Add negative regex tests (keyboard interrupt, process interrupted,
interrupted system call) to verify the interrupt classifier doesn't
match non-session interrupt messages.

Tests: 118/118 pass (3 new assertions for buildStallAutoKillConfig +
3 negative regex assertions).

[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(QueueProcessor): add ensureTerminalStatus backstop regression test (zts212653#1145)

Addresses codex review P2: the QueueProcessor terminal backstop had no
test coverage. New test verifies that when the catch-block update(failed)
throws (e.g. Redis blip), ensureTerminalStatus fires a CAS write with
expectedStatus=running to ensure the record reaches terminal status.

Also exercises the .get() path and confirms the backstop reads the
record before attempting the CAS update.

[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): backstop test false-green — trigger on patch semantics not call count

The zts212653#1145 backstop test was a false green: updateCallCount===1 fired on the
userMessageId backfill (call #1), not the catch-block's failed write (call #3).
The throw never reached the intended code path, and get() was hardcoded to
return 'running' regardless of actual state.

Fix: trigger throw on `status==='failed' && !expectedStatus` (catch-block
signature) instead of call count. Track record state so get() returns the
realistic status after each successful update. Verify the full chain:
catch-block throw → record stays running → CAS backstop → terminal failed.

Addresses codex review P2 on 37ad884.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bouillipx pushed a commit that referenced this pull request Jul 14, 2026
…212653#1103)

* fix(F113): include drive-letter layer in directory breadcrumb

Windows drive paths (e.g. D:\Projects) were rendering as
"此电脑 > Projects" — the drive root segment was silently swallowed
by pathToSegments because parts[0] ("D:") was used only as the
accumulation seed, never pushed as a clickable segment. The user
could not click back to the drive root, and the breadcrumb read
wrong.

Fix: extract windowsDriveSegments() helper that emits the drive
root (D:\, with trailing separator so backend realpath resolves
the drive, not cwd-on-drive) as its own segment. Render layer
shows the friendly drive label (本地磁盘 (D:)) for any drive-root
segment, not just the leaf — so "此电脑 > 本地磁盘 (D:) > Projects"
throughout navigation. Unix and under-home paths unchanged.

Also adds NEXT_DIST_DIR env to next.config distDir so parallel dev
instances can isolate their build cache (default .next unchanged).

Tests: +1 frontend case for drive-letter breadcrumb; backend
node:test suite 24/24 green (drives endpoint + listAvailableDrives).

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

* fix(F113): show drive letter as "D:" in breadcrumb (VS Code style)

co-creator feedback: breadcrumb rendered "此电脑 > D:\ > AI" — the drive
segment showed the raw path "D:\" with trailing separator. Expected "D:"
without backslash, matching VS Code / common file-picker convention.

Root cause: driveRootLabel(seg.path, drives) fell back to the raw absPath
("D:\") whenever the drives list hadn't been lazily loaded yet — which is
the common case when a user lands directly on a drive subdirectory without
first visiting the "此电脑" drive-picker view.

Fix: use seg.label directly for drive segments. windowsDriveSegments
already sets label = parts[0] = "D:" (separator stripped), so the drive
segment now reads "D:" regardless of whether drives have been fetched.
This also drops the dependency on the lazily-loaded drives list for
breadcrumb rendering.

Removed dead code: isWindowsDriveRoot + driveRootLabel (no longer called
after the change). tsc clean, 16/16 tests pass.

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

* fix(F113): address codex review P2 — server-side isWindows gate + hide create-folder in drives view

1. Gate "此电脑" entry on the SERVER's filesystem platform (derived from the
   browsed path) instead of the browser client's userAgent. A macOS/Linux
   client browsing a Windows-hosted server now correctly gets drive switching;
   a Windows client against a non-Windows server no longer shows an empty
   picker. The browser browses the API server's filesystem, so the gate must
   reflect server platform, not client UA.

2. Hide the create-folder button in drives view — there is no current
   directory, so handleCreateDir would post a stale parentPath from the
   previously browsed directory.

tsc clean, 16/16 frontend tests green.

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

* fix(F113): clear create-folder state on entering drives view (R2 review)

R2 review (zts212653, CHANGES_REQUESTED): the codex P2 fix only hid the
new-folder button in drives view, but an already-open inline create-folder
editor survived the transition. handleCreateDir would then post
parentPath: browseResult.current from the stale previous directory - the
same wrong-location filesystem mutation the earlier review was trying to
eliminate.

Fix: enterDrivesView now clears create-folder state (creatingDir, newDirName,
mkdirError) alongside the existing error/info banner reset, so no inline
editor can survive the transition into 此电脑.

Regression test: "start create folder -> enter drives view -> no create
controls remain" (Red -> Green). tsc clean, 17/17 frontend tests green.

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

* fix(F113): hide stale directory breadcrumb in drives view (R2 P1)

R2 follow-up (zts212653): when entering 此电脑 drives view, the old
directory breadcrumb segments (e.g. "此电脑 > D: > Projects") kept
rendering unconditionally via segments.map(), so the stale previous
directory was painted as the current level alongside the drive grid.

Fix: gate the breadcrumb segments render on `view !== 'drives'`. The
drives view shows only its own "此电脑" root, not the stale path.

Regression test extended: "start create folder -> enter drives view"
now also asserts the old directory segment ("Projects") is not present
in the breadcrumb text (Red -> Green). tsc clean, 17/17 tests green.

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

* chore(F113): revert NEXT_DIST_DIR scope (out of drive picker)

Maintainer R2 follow-up: the NEXT_DIST_DIR env (packages/web/next.config.js
distDir) and the .gitignore .next-*/ rule were acceptance-infra changes
(parallel dev instances for isolated verification), not part of the drive
picker feature. Reverted both to upstream/main state to keep this PR scoped
to the drive picker only.

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

* fix(F113): injectable drive probe + deterministic tests (R3 P1 #1)

Maintainer R3 review: the Windows success path was never actually tested.
Windows Smoke CI doesn't run pick-directory.test.js, and on non-Windows CI
listAvailableDrives('win32') could return [] because realpathSync throws on
every probe - so the test never proved C:/D: discovery works.

Fix: listAvailableDrives now accepts an injectable probeRealpath function
(defaults to fs.realpathSync). Added 3 deterministic tests using a mock
probe: mounted drives (C:/D:) return correct letter/path/label, inaccessible
drives are skipped, and the returned path is the resolved real path not the
probed root. 27/27 backend tests green.

Also fixed: root path construction (`${letter}:\`) was corrupted during an
earlier edit (lost the backslash) - now correct.

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

* refactor(F113): extract useDrivesLoader + state machine (R3 P1#2 + P2#4 part 1)

Maintainer R3 review P1#2: loading/empty/failure were conflated.
drivesLoadedRef was set before the request resolved, so one transient failure
permanently disabled retry until remount, and the first render showed
"未发现可用磁盘" while the fetch was still pending.

Fix: extract useDrivesLoader into its own module (use-drives-loader.ts) with
an explicit `idle | loading | ready | error` state machine. The drives view
now renders distinct states: "正在加载磁盘列表..." (loading), "磁盘列表加载失败"
+ retry button (error), "未发现可用磁盘" (ready + empty), or the drive grid
(ready + populated). Retry resets to idle, re-triggering the fetch.

This also begins P2#4 (split DirectoryBrowser.tsx): the drive loader/state
machine is now a focused module. Segment parsing + icons extraction follows.
DirectoryBrowser.tsx: 611 -> 566 lines. tsc clean, 17/17 tests green.

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

* refactor(F113): extract segment parsing + icons (R3 P2#4 part 2)

Extracted from DirectoryBrowser.tsx:
- directory-segments.ts: windowsDriveSegments, pathToSegments, buildBrowseUrl,
  shouldFallbackToHome + BrowseEntry/BrowseResult/BreadcrumbSegment types
- directory-browser-icons.tsx: HomeIcon, PcIcon, DriveIcon, FolderIcon,
  TerminalIcon

DirectoryBrowser.tsx: 583 -> 437 lines. Still above the 350-line hard limit;
breadcrumb/drives-view sub-components extraction follows. tsc clean, 17/17
tests green.

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

* refactor(F113): extract DirectoryBreadcrumb component (R3 P2#4 part 3)

Extracted the breadcrumb row + new-folder button into DirectoryBreadcrumb
component (directory-breadcrumb.tsx), reducing DirectoryBrowser.tsx from 437
to 368 lines. The breadcrumb renders 此电脑 entry + VS Code-style segments +
new-folder button, receiving view state + navigation callbacks as props.

tsc clean, 17/17 tests green.

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

* refactor(F113): extract DrivesView + meet 350-line limit (R3 P2#4 done)

Extracted the drives-picker grid (loading/error/empty/populated states) into
DrivesView component (drives-view.tsx). DirectoryBrowser.tsx is now 326 lines,
satisfying the 350-line hard limit (down from 611).

Full split summary (P2#4):
- use-drives-loader.ts: drive loader + idle|loading|ready|error state machine (P1#2)
- directory-segments.ts: Windows segment parsing + types
- directory-browser-icons.tsx: 5 icons
- directory-breadcrumb.tsx: breadcrumb row + new-folder button
- drives-view.tsx: drives grid + loading/error/empty states

DirectoryBrowser.tsx now focuses on the directory listing + create-folder
inline editor + path input, delegating root-navigation concerns to the
extracted modules. tsc clean, 17/17 tests green.

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

* feat(F113): server-owned drives capability + path policy filter (R3 P2#3 part 1)

Maintainer R3 review P2#3: capability/path policy must come from the server,
not UI-inferred path shape (misses UNC roots).

Backend changes:
- drives endpoint now returns `{ drives, isWindows }` where isWindows is
  process.platform === 'win32' (server-owned capability, not path heuristic)
- drives are filtered by project-path policy (isUnderAllowedRoot) so an
  allowlist deployment doesn't advertise drive roots that /browse will reject

Frontend (part 2, follows): useDrivesLoader will consume isWindows from the
drives endpoint instead of inferring from browseResult.current path shape.

27/27 backend tests green.

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

* fix(F113): remove unused imports + biome format (R3 CI fix)

CI Lint + Build failed: DirectoryBrowser.tsx had unused imports (BrowseEntry,
HomeIcon, PcIcon, DriveIcon) left over after the module extraction, and
pick-directory.test.js had a format issue. Fixed by removing unused imports
and running biome format --write.

tsc clean, 17/17 frontend + 27/27 backend tests green, biome error-level clean.

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

* feat(F113): server-owned capability end-to-end + Windows CI + state tests (R4)

R4 review closures:

1. Server capability consumed by frontend (P2#3 end-to-end): browse endpoint
   returns isWindows: process.platform === 'win32'. DirectoryBrowser uses
   browseResult.isWindows instead of path-shape regex heuristic (misses UNC
   roots). 2 regression tests: isWindows=false hides 此电脑, true shows it.

2. Windows CI: pick-directory.test.js wired into windows-smoke.yml.

3. State machine regression: loading (pending->loading->ready) + error+retry
   tests. Backend isWindows contract assertions for /drives and /browse.

tsc clean, 21/21 frontend + 28/28 backend tests green, biome format clean.

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

* test(F113): retry->ready + empty-state regression coverage (R5)

R5 review: the error/retry test stopped at asserting the button exists.
Extended to exercise the full retry transition (error -> click retry -> idle
-> loading -> ready with drive grid) and added an independent empty-state
test (ready + [] -> 未发现可用磁盘, not loading/error).

useDrivesLoader state transitions now fully covered:
- idle -> loading -> ready (loading test)
- loading -> error (error test)
- error -> retry -> ready (extended retry test)
- ready + [] -> 未发现可用磁盘 (empty-state test)

22/22 frontend tests green.

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

* fix: clear stale path in drive picker root

Why:
Entering the virtual 此电脑 drive picker root is not browsing a concrete project directory. Keeping the previous directory in the manual path field and selected-path footer made the UI look stale and allowed accidental creation against the old path.

What:
- Clear DirectoryBrowser pathInput when entering drives view.
- Notify DirectoryPickerModal when the browser is on a virtual location.
- Clear selectedPath in that state so create stays disabled until a real directory is selected.
- Add regressions for both the browser input and modal selected-path footer.

* fix: fence late cwd selection in drive picker

Why: A delayed cwd initializer could treat the virtual 此电脑 root as uninitialized state and reselect a stale directory, enabling thread creation while the drive grid was still visible.

[宪宪/gpt-5.5🐾]

* fix: surface this pc in directory list

Why: Windows users need a discoverable route from any browsed directory back to the drive picker; breadcrumb-only access was too easy to miss, making other disks effectively unreachable from the modal.

Verification: pnpm --dir packages/web exec vitest run src/components/ThreadSidebar/__tests__/directory-browser.test.ts; pnpm --dir packages/web exec vitest run src/components/ThreadSidebar/__tests__/directory-picker-modal.test.ts; pnpm --dir packages/web exec tsc --noEmit.

[宪宪/gpt-5.5🐾]

* refactor: extract this pc list entry

Why: R8 flagged DirectoryBrowser over the repository line-count cap after adding the discoverable drive-switching row. Extracting the row presentation keeps the behavior unchanged while returning DirectoryBrowser below the hard cap.

Verification: directory-browser.test.ts 23 passed; directory-picker-modal.test.ts 26 passed; tsc --noEmit passed; biome check on touched files exited 0 with pre-existing test warnings only.

[宪宪/gpt-5.5🐾]

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.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