diff --git a/.dir-exceptions.json b/.dir-exceptions.json index 2ac95d3aeb..19f8d5d615 100644 --- a/.dir-exceptions.json +++ b/.dir-exceptions.json @@ -3,71 +3,57 @@ { "path": "packages/api/src/domains/cats/services/agents/routing", "owner": "opus-48", - "reason": "Routing 25 files(agent routing + artifact-tracking + F232 thread-artifacts-aggregator)。与 invocation(25)/providers(32) 同属 agents 子域债务,ADR-010 该子拆分但 scope 不含 F232。F232 加 aggregator 顶过 25 线,time-bound 豁免待 routing 子拆分(记 F23 Phase 2)。", - "expiresAt": "2026-06-30", + "reason": "Routing 25 files(agent routing + artifact-tracking + F232 thread-artifacts-aggregator)。与 invocation(27)/providers(40) 同属 agents 子域债务,ADR-010 该子拆分但 scope 不含 F232。THIRD-ROUND unblock 由同 PR 完成 utils 真拆支撑;time-bound 豁免待 routing 子拆分(记 F23 Phase 2)。", + "expiresAt": "2026-07-31", "ticket": "F23-followup" }, { "path": "packages/api/src/routes", "owner": "opus", - "reason": "Routes 按功能已分文件(147 files),ADR-010 重构 scope 不含 routes,需要按 HTTP verb / domain 重新评估子拆分边界。SECOND-ROUND unblock(首轮 a4e81b8791 续期到 2026-06-01)。Follow-up split plan 记在 F23 § Phase 2 follow-up。", - "expiresAt": "2026-06-30", + "reason": "Routes 按功能已分文件(171 files),ADR-010 重构 scope 不含 routes,需要按 HTTP verb / domain 重新评估子拆分边界。THIRD-ROUND unblock 由同 PR 完成 utils 真拆支撑。Follow-up split plan 记在 F23 § Phase 2 follow-up。", + "expiresAt": "2026-07-31", "ticket": "F23-followup" }, { "path": "packages/api/src/config", "owner": "opus", - "reason": "Config 38 files(cat-budgets/cat-config/env 等独立配置项)。需要先做 config 域 audit 再确定子拆分边界。SECOND-ROUND unblock(首轮 a4e81b8791 续期到 2026-06-01)。Follow-up split plan 记在 F23 § Phase 2 follow-up。", - "expiresAt": "2026-06-30", + "reason": "Config 39 files(cat-budgets/cat-config/env 等独立配置项)。需要先做 config 域 audit 再确定子拆分边界。THIRD-ROUND unblock 由同 PR 完成 utils 真拆支撑。Follow-up split plan 记在 F23 § Phase 2 follow-up。", + "expiresAt": "2026-07-31", "ticket": "F23-followup" }, { "path": "packages/api/src/domains/cats/services/agents/invocation", "owner": "opus", - "reason": "Invocation 控制面 25 files;按 queue/registry/tracker/progress/reconciliation/delivery/auth 子域拆分。SECOND-ROUND unblock(首轮 4136847b10 砚砚 2026-05-18 续期到 2026-06-01)。Follow-up split plan 记在 F23 § Phase 2 follow-up。", - "expiresAt": "2026-06-30", + "reason": "Invocation 控制面 27 files;按 queue/registry/tracker/progress/reconciliation/delivery/auth 子域拆分。THIRD-ROUND unblock 由同 PR 完成 utils 真拆支撑。Follow-up split plan 记在 F23 § Phase 2 follow-up。", + "expiresAt": "2026-07-31", "ticket": "F23-followup" }, { "path": "packages/api/src/domains/cats/services/agents/providers", "owner": "opus", - "reason": "Agent providers 32 files;按 agents/event-transforms/carriers/image/configs 子域拆分。SECOND-ROUND unblock(首轮 4136847b10 砚砚 2026-05-18 续期到 2026-06-01)。Follow-up split plan 记在 F23 § Phase 2 follow-up。", - "expiresAt": "2026-06-30", + "reason": "Agent providers 40 files;按 agents/event-transforms/carriers/image/configs 子域拆分。THIRD-ROUND unblock 由同 PR 完成 utils 真拆支撑。Follow-up split plan 记在 F23 § Phase 2 follow-up。", + "expiresAt": "2026-07-31", "ticket": "F23-followup" }, { "path": "packages/api/src/domains/cats/services/stores/ports", "owner": "codex", - "reason": "F231 Phase C 新增 ProfileUpdateProposalStore port 后,stores/ports 达到 25 files;当前 store port 集中目录仍是既有模式,Phase C scope 不做跨 store-port 拆分。需在 F23/F231 follow-up 中按 proposal/profile/session 等子域拆分 ports。", - "expiresAt": "2026-06-30", + "reason": "F231 Phase C 新增 ProfileUpdateProposalStore port 后,stores/ports 达到 26 files;当前 store port 集中目录仍是既有模式,Phase C scope 不做跨 store-port 拆分。需在 F23/F231 follow-up 中按 proposal/profile/session 等子域拆分 ports。", + "expiresAt": "2026-07-31", "ticket": "F231/F23-followup" }, { "path": "packages/api/src/domains/memory", "owner": "gpt52", - "reason": "Memory 域现有 72 个非 index.ts 文件,职责已横跨 indexing/query/governance/library;SECOND-ROUND unblock(首轮续期到 2026-06-15),继续按真实边界拆子目录。", - "expiresAt": "2026-06-30", + "reason": "Memory 域现有 89 个非 index.ts 文件,职责已横跨 indexing/query/governance/library;继续按真实边界拆子目录。", + "expiresAt": "2026-07-31", "ticket": "F102" }, - { - "path": "packages/api/src/utils", - "owner": "codex", - "reason": "API utils 31 files;按 cli/process/media/paths/network/parsing/skills 子域拆分(详见 F23 § Phase 2)。SECOND-ROUND unblock(首轮 4136847b10 砚砚 2026-05-18 续期到 2026-06-01)。Follow-up split plan 记在 F23 § Phase 2 follow-up。", - "expiresAt": "2026-06-30", - "ticket": "F23-followup" - }, - { - "path": "packages/api/src/infrastructure/harness-eval", - "owner": "opus", - "reason": "F192 socio-technical harness eval 当前 29 .ts(Phase F capability-wakeup 一批 eval-capability-wakeup-* 加入超 error=25);SECOND-ROUND unblock(首轮续期到 2026-06-15),后续按 capability-wakeup / a2a / domain / hub 子域拆分。详见 F192 feat doc Known Debt + GitHub issue。", - "expiresAt": "2026-06-30", - "ticket": "F192" - }, { "path": "packages/api/src/domains/cats/services/stores/redis", "owner": "opus", "reason": "F235 RedisCommunityIssueDraftStore 加入后 redis/ 达 25 files;与 ports/(26) 同源——每个 store port 一个 Redis impl。需在 F23 follow-up 中按 proposal/community/session 子域拆分。", - "expiresAt": "2026-06-30", + "expiresAt": "2026-07-31", "ticket": "F23-followup" } ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8734fc6786..d91b1886e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: - 'designs/**' - 'assets/**' pull_request: - branches: [main] + branches: [main, develop] paths-ignore: - 'docs/**' - '*.md' diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml index 928ee6419e..34a543ea34 100644 --- a/.github/workflows/windows-smoke.yml +++ b/.github/workflows/windows-smoke.yml @@ -9,7 +9,7 @@ name: Windows Smoke on: pull_request: - branches: [main] + branches: [main, develop] paths-ignore: - 'docs/**' - '*.md' diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7d57bf7409..e1890f5865 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -66,6 +66,7 @@ created: 2026-02-26 | F203 | Native System Prompt L0 — 压缩免疫核心规则注入 | in-progress | Ragdoll Opus 4.7 | internal | [F203](features/F203-native-system-prompt-l0.md) | | F204 | Weixin MP Publisher Plugin — 微信公众号文章发布插件 | review | community @mindfn + maintainers | community [#688](https://github.com/zts212653/clowder-ai/pull/688) | [F204](features/F204-weixin-mp-publisher-plugin.md) | | F205 | MediaHub Video Provider Plugins — 视频生成/分析插件 | spec | community @mindfn + maintainers | community [#689](https://github.com/zts212653/clowder-ai/pull/689) | [F205](features/F205-video-provider-plugins.md) | +| F207 | AI Family Office — 个人投资学习基建 | spec | Ragdoll | internal | [F207](features/F207-personal-finance-infra.md) | | F208 | Capability Profile Routing — 能力画像档案 + 认知路由 | spec | Ragdoll | internal | [F208](features/F208-capability-profile-routing.md) | | F193 | Cross-Thread Communication Unification (Phase E: 发现即投递) | in-progress | Ragdoll (Opus 4.6) | internal | [F193](features/F193-cross-thread-comm-unification.md) | | F210 | Gemini CLI to Antigravity CLI Migration | in-progress | Maine Coon/Maine Coon | internal | [F210](features/F210-antigravity-cli-migration.md) | diff --git a/docs/features/F023-directory-corrosion-defense.md b/docs/features/F023-directory-corrosion-defense.md index 321b814e99..ddf218ee72 100644 --- a/docs/features/F023-directory-corrosion-defense.md +++ b/docs/features/F023-directory-corrosion-defense.md @@ -40,6 +40,8 @@ created: 2026-02-26 这条 gate 落到 `docs/SOP.md` 「outbound sync 基线修复」段(this PR 不顺手改 SOP,由 Phase 2 第一个真拆 PR 一起落)。 +**2026-07-07 PR #40 CI unblock**:满足第三轮 unblock 条件 (b)。同 PR 已将 `packages/api/src/utils` 按 `cli/process/media/paths/network/parsing/skills` 真拆,删除对应 `.dir-exceptions.json` 条目;`packages/api/src/infrastructure/harness-eval` 已低于阈值,也删除过期例外。其余仍超阈值目录仅续期到 2026-07-31。 + ### 5 目录 concrete split map | 目录 | 文件数 | Owner | 子目录拆分方案 | Target | diff --git a/packages/api/config/public-test-exclusions.json b/packages/api/config/public-test-exclusions.json index dc307748e5..37824a9879 100644 --- a/packages/api/config/public-test-exclusions.json +++ b/packages/api/config/public-test-exclusions.json @@ -8,7 +8,7 @@ "reason": "Redis isolation and persistence tests are internal harness coverage, not part of the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "concurrent-fault-drill", @@ -17,7 +17,7 @@ "reason": "Fault-drill stress coverage is too heavy for the public gate and stays in internal validation.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "task-progress-store", @@ -26,7 +26,7 @@ "reason": "Task progress store behavior depends on internal persistence surfaces not exported to the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "session-strategy-phase3", @@ -35,7 +35,7 @@ "reason": "Phase 3 session strategy assertions cover internal rollout behavior outside the public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "signal-article-store", @@ -44,7 +44,7 @@ "reason": "Signal article store cases exercise source-only data plumbing not guaranteed in the public export.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "persistence-fault-drill", @@ -53,7 +53,7 @@ "reason": "Persistence fault drills are heavyweight stress scenarios reserved for internal validation.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "cursor-store-atomicity", @@ -62,7 +62,7 @@ "reason": "Atomic cursor store checks cover internal durability mechanics not enforced by the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "workflow-sop-store", @@ -71,7 +71,7 @@ "reason": "Workflow SOP store tests rely on internal governance persistence surfaces excluded from the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "dare-agent-service", @@ -80,7 +80,7 @@ "reason": "DARE agent service coverage is internal-only agent runtime behavior, not public gate surface.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "dare-l1-acceptance", @@ -89,7 +89,7 @@ "reason": "DARE L1 acceptance exercises internal harness scenarios not exported publicly.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "codex-agent-service", @@ -98,7 +98,7 @@ "reason": "Codex agent runtime service tests depend on internal carrier/harness wiring outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "kimi-agent-service", @@ -107,7 +107,7 @@ "reason": "Kimi agent service coverage is internal runtime behavior, not public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "claude-settings-hooks", @@ -116,7 +116,7 @@ "reason": "Claude settings hook assertions depend on private runtime fixtures unavailable in public export.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "game-store", @@ -125,7 +125,7 @@ "reason": "Game store tests rely on non-exported local fixtures and are kept out of the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "memory-tests", @@ -134,7 +134,7 @@ "reason": "Memory package tests cover internal recall/index behavior outside the public gate promise.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "cross-cat-context", @@ -143,7 +143,7 @@ "reason": "Cross-cat context routing is internal collaboration harness behavior, not public gate surface.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "thread-wiring", @@ -152,7 +152,7 @@ "reason": "Thread wiring assertions cover internal callback plumbing outside the public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "integration-wiring", @@ -161,7 +161,7 @@ "reason": "Integration wiring checks depend on internal connection topology not exported to public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "shared-state-wiring", @@ -170,7 +170,7 @@ "reason": "Shared-state wiring is internal runtime glue not part of the public gate guarantee.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "signal-fetcher-launchd", @@ -179,7 +179,7 @@ "reason": "launchd-specific signal fetcher coverage depends on private macOS fixtures and stays internal.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "reflection-capsule-m3", @@ -188,7 +188,7 @@ "reason": "Reflection capsule M3 tests cover internal memory/harness behavior outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "workspace-project-context", @@ -197,7 +197,7 @@ "reason": "Workspace project context assertions depend on source-only repo structure and local project wiring.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "projects-setup", @@ -206,7 +206,7 @@ "reason": "Project setup tests cover internal bootstrap flows not guaranteed in the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "projects-mkdir", @@ -215,7 +215,7 @@ "reason": "Project directory creation coverage depends on source-only filesystem conventions.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "governance-status", @@ -224,7 +224,7 @@ "reason": "Governance status assertions are source-owned workflow coverage, not public gate behavior.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "governance-pack", @@ -233,7 +233,7 @@ "reason": "Governance pack assertions validate source-only sync content and are intentionally excluded from public gate.", "owner": "@zts212653", "introducedBy": "069d0f0fb", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "pack-integration", @@ -242,7 +242,7 @@ "reason": "Pack integration coverage exercises internal source packaging rules outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "project-setup-flow", @@ -251,7 +251,7 @@ "reason": "Project setup flow behavior is internal bootstrap coverage, not public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "process-liveness-probe", @@ -260,7 +260,7 @@ "reason": "Process liveness probe timing is too contention-sensitive for the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "expedition-bootstrap", @@ -269,7 +269,7 @@ "reason": "Expedition bootstrap coverage depends on source-owned harness flows not exported publicly.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "rules-route", @@ -278,7 +278,7 @@ "reason": "Rules route assertions depend on source-only rule artifacts stripped from public export.", "owner": "@zts212653", "introducedBy": "4243948da", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "root-md-slim", @@ -287,7 +287,7 @@ "reason": "Root markdown slim tests rely on source-specific Chinese anchors not preserved in public export.", "owner": "@zts212653", "introducedBy": "7a300704a", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "audit-cc-system-prompt", @@ -296,7 +296,7 @@ "reason": "System prompt audit depends on internal L0 prompt assets not shipped in public export.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "f188-cold-start-fixtures", @@ -305,7 +305,7 @@ "reason": "F188 cold-start fixture coverage depends on internal fixture files absent from public export.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "f188-harness-consistency", @@ -314,7 +314,7 @@ "reason": "F188 harness consistency checks rely on internal fixtures and remain source-only.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "orphan-chrome-cleaner", @@ -323,7 +323,7 @@ "reason": "Orphan Chrome cleaner assertions are sensitive to local path sanitization and private fixtures.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "capabilities-route", @@ -332,7 +332,7 @@ "reason": "Managed MCP path realignment currently fails in public gate and must be tracked as a real product regression.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "antigravity-run-command-executor", @@ -341,7 +341,7 @@ "reason": "Antigravity run-command executor timing is unstable on CI and remains out of the public gate until hardened.", "owner": "@zts212653", "introducedBy": "0340783c6", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "f203-phase-i-opencode-l0", @@ -350,7 +350,7 @@ "reason": "F203 opencode L0 coverage depends on internal runtime files not exported publicly.", "owner": "@zts212653", "introducedBy": "9c4f26fde", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "github-schedule-factories", @@ -359,7 +359,7 @@ "reason": "GitHub schedule factory assertions are source-owned public-sync governance coverage, not runtime gate behavior.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "harness-eval-hub-read-model", @@ -368,7 +368,7 @@ "reason": "Eval hub read-model coverage exercises source-owned harness governance surfaces outside the public gate.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" }, { "id": "harness-eval-merge-gate-provenance-contract", @@ -377,7 +377,7 @@ "reason": "Merge-gate provenance contract tests are source-owned governance coverage, not public runtime behavior.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-06-30" + "expiresOn": "2026-07-31" } ] } diff --git a/packages/api/src/config/ConfigRegistry.ts b/packages/api/src/config/ConfigRegistry.ts index 37a728dd5d..359dad4880 100644 --- a/packages/api/src/config/ConfigRegistry.ts +++ b/packages/api/src/config/ConfigRegistry.ts @@ -7,7 +7,7 @@ */ import { catRegistry } from '@cat-cafe/shared'; -import { DEFAULT_CLI_TIMEOUT_MS, readCliTimeoutMsFromEnv } from '../utils/cli-timeout.js'; +import { DEFAULT_CLI_TIMEOUT_MS, readCliTimeoutMsFromEnv } from '../utils/cli/cli-timeout.js'; import { configStore } from './ConfigStore.js'; import { getAllCatBudgets } from './cat-budgets.js'; import { getCoCreatorConfig } from './cat-config-loader.js'; diff --git a/packages/api/src/config/capabilities/capability-orchestrator.ts b/packages/api/src/config/capabilities/capability-orchestrator.ts index bb2d7f201f..df1b93bd68 100644 --- a/packages/api/src/config/capabilities/capability-orchestrator.ts +++ b/packages/api/src/config/capabilities/capability-orchestrator.ts @@ -16,7 +16,7 @@ import { homedir } from 'node:os'; import { delimiter, dirname, extname, join, relative, resolve, sep } from 'node:path'; import type { CapabilitiesConfig, CapabilityEntry, McpServerDescriptor } from '@cat-cafe/shared'; import { catRegistry } from '@cat-cafe/shared'; -import { resolveCatCafeSkillsSource } from '../../utils/skill-source.js'; +import { resolveCatCafeSkillsSource } from '../../utils/skills/skill-source.js'; import { migrateCapabilitiesV1ToV2 } from '../governance/capabilities-migration.js'; import { cleanStaleClaudeProjectOverrides, diff --git a/packages/api/src/config/capabilities/capability-write-guards.ts b/packages/api/src/config/capabilities/capability-write-guards.ts index 48d060b08b..0c699ad94c 100644 --- a/packages/api/src/config/capabilities/capability-write-guards.ts +++ b/packages/api/src/config/capabilities/capability-write-guards.ts @@ -1,5 +1,5 @@ import type { FastifyRequest } from 'fastify'; -import { isLoopbackAddress } from '../../utils/loopback-request.js'; +import { isLoopbackAddress } from '../../utils/network/loopback-request.js'; import { resolveOwnerGate } from '../../utils/owner-gate.js'; import { REDACTED_CAPABILITY_SECRET } from './capability-redaction.js'; diff --git a/packages/api/src/config/capabilities/startup-cli-config.ts b/packages/api/src/config/capabilities/startup-cli-config.ts index 57e92eecbf..75c09f240b 100644 --- a/packages/api/src/config/capabilities/startup-cli-config.ts +++ b/packages/api/src/config/capabilities/startup-cli-config.ts @@ -1,6 +1,6 @@ import { homedir } from 'node:os'; import { join } from 'node:path'; -import { findMonorepoRoot } from '../../utils/monorepo-root.js'; +import { findMonorepoRoot } from '../../utils/paths/monorepo-root.js'; import { type CliConfigPaths, generateCliConfigs, diff --git a/packages/api/src/config/cat-voices.ts b/packages/api/src/config/cat-voices.ts index f9f5e3ac2c..dab4fb7959 100644 --- a/packages/api/src/config/cat-voices.ts +++ b/packages/api/src/config/cat-voices.ts @@ -19,7 +19,7 @@ import { homedir } from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import type { VoiceConfig } from '@cat-cafe/shared'; import { catRegistry } from '@cat-cafe/shared'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; import { resolveBreedId } from './breed-resolver.js'; import { getAllCatIdsFromConfig, loadCatConfig } from './cat-config-loader.js'; diff --git a/packages/api/src/config/connector-secret-updater.ts b/packages/api/src/config/connector-secret-updater.ts index c4b8fb1df0..be072d84b0 100644 --- a/packages/api/src/config/connector-secret-updater.ts +++ b/packages/api/src/config/connector-secret-updater.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { applyEnvUpdatesToFile } from '../routes/config.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { configEventBus, createChangeSetId } from './config-event-bus.js'; export interface ConnectorSecretUpdate { diff --git a/packages/api/src/config/connector-secret-write-guards.ts b/packages/api/src/config/connector-secret-write-guards.ts index ac924d2b7b..a857e08895 100644 --- a/packages/api/src/config/connector-secret-write-guards.ts +++ b/packages/api/src/config/connector-secret-write-guards.ts @@ -1,6 +1,6 @@ import type { FastifyRequest } from 'fastify'; import { normalizeTelegramBotToken } from '../infrastructure/connectors/telegram-token.js'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; import { isConnectorSecret } from './connector-secrets-allowlist.js'; diff --git a/packages/api/src/config/env-registry.ts b/packages/api/src/config/env-registry.ts index 1c27278280..1023e4d54f 100644 --- a/packages/api/src/config/env-registry.ts +++ b/packages/api/src/config/env-registry.ts @@ -12,7 +12,7 @@ * The "环境 & 文件" tab picks it up automatically. */ -import { DEFAULT_CLI_TIMEOUT_LABEL } from '../utils/cli-timeout.js'; +import { DEFAULT_CLI_TIMEOUT_LABEL } from '../utils/cli/cli-timeout.js'; export type EnvCategory = | 'server' diff --git a/packages/api/src/config/governance/governance-bootstrap.ts b/packages/api/src/config/governance/governance-bootstrap.ts index 3a70606b48..e858b52051 100644 --- a/packages/api/src/config/governance/governance-bootstrap.ts +++ b/packages/api/src/config/governance/governance-bootstrap.ts @@ -16,8 +16,8 @@ import { STANDARD_MOUNT_POINT_IDS, } from '@cat-cafe/shared'; import { updateSkillMountPaths, writeSkillsSyncState } from '../../skills/skill-sync-config.js'; -import { pathsEqual } from '../../utils/project-path.js'; -import { computeSourceManifestHash } from '../../utils/skill-source.js'; +import { pathsEqual } from '../../utils/paths/project-path.js'; +import { computeSourceManifestHash } from '../../utils/skills/skill-source.js'; import { readCapabilitiesConfig, writeCapabilitiesConfig } from '../capabilities/capability-orchestrator.js'; import { readMountRules } from '../mount/mount-rules-store.js'; import type { Provider } from './governance-pack.js'; diff --git a/packages/api/src/config/governance/governance-preflight.ts b/packages/api/src/config/governance/governance-preflight.ts index 7b231baea0..f7f7266e15 100644 --- a/packages/api/src/config/governance/governance-preflight.ts +++ b/packages/api/src/config/governance/governance-preflight.ts @@ -8,7 +8,7 @@ */ import { lstat, readdir, readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { isSameProject } from '../../utils/monorepo-root.js'; +import { isSameProject } from '../../utils/paths/monorepo-root.js'; import type { Provider } from './governance-pack.js'; import { MANAGED_BLOCK_START } from './governance-pack.js'; import { GovernanceRegistry } from './governance-registry.js'; diff --git a/packages/api/src/config/governance/governance-registry.ts b/packages/api/src/config/governance/governance-registry.ts index 49a581528d..650e5a53a3 100644 --- a/packages/api/src/config/governance/governance-registry.ts +++ b/packages/api/src/config/governance/governance-registry.ts @@ -9,7 +9,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { relative, resolve, sep } from 'node:path'; import type { GovernanceHealthSummary, GovernancePackMeta } from '@cat-cafe/shared'; -import { pathsEqual } from '../../utils/project-path.js'; +import { pathsEqual } from '../../utils/paths/project-path.js'; import { GOVERNANCE_PACK_VERSION } from './governance-pack.js'; const REGISTRY_DIR = '.cat-cafe'; diff --git a/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts b/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts index c9a210794b..edcdec7484 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/InvocationQueue.ts @@ -48,6 +48,8 @@ export interface QueueEntry { suggestedSkill?: string; /** True only for connector wakes backed by verified external callback/tracking coverage. */ eventDrivenExternalWaitCoverage?: boolean; + /** Canonical external ids covered by the verified callback/tracking path. */ + eventDrivenExternalWaitCoverageKeys?: string[]; callerTraceContext?: CallerTraceContext; /** Explicit A2A trigger message for stream reply threading. */ a2aTriggerMessageId?: string; @@ -188,6 +190,14 @@ export class InvocationQueue { if (input.eventDrivenExternalWaitCoverage) { existing.eventDrivenExternalWaitCoverage = true; } + if (input.eventDrivenExternalWaitCoverageKeys?.length) { + existing.eventDrivenExternalWaitCoverageKeys = [ + ...new Set([ + ...(existing.eventDrivenExternalWaitCoverageKeys ?? []), + ...input.eventDrivenExternalWaitCoverageKeys, + ]), + ]; + } } const position = q.findIndex((entry) => entry.id === existing.id); return { @@ -228,6 +238,9 @@ export class InvocationQueue { continuationKey: input.continuationKey, suggestedSkill: input.suggestedSkill, eventDrivenExternalWaitCoverage: input.eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: input.eventDrivenExternalWaitCoverageKeys + ? [...input.eventDrivenExternalWaitCoverageKeys] + : undefined, callerTraceContext: input.callerTraceContext, a2aTriggerMessageId: input.a2aTriggerMessageId, position: undefined, diff --git a/packages/api/src/domains/cats/services/agents/invocation/InvocationTracker.ts b/packages/api/src/domains/cats/services/agents/invocation/InvocationTracker.ts index e1151f34b8..9e7f7c22c2 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/InvocationTracker.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/InvocationTracker.ts @@ -10,7 +10,7 @@ */ import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { resolveCliTimeoutMs } from '../../../../../utils/cli-timeout.js'; +import { resolveCliTimeoutMs } from '../../../../../utils/cli/cli-timeout.js'; const log = createModuleLogger('invocation-tracker'); diff --git a/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts b/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts index 634ce7dc4e..18c7e678f3 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/QueueProcessor.ts @@ -7,7 +7,7 @@ * - processNext(用户级):co-creator手动触发处理自己的下一条 */ -import { resolveCliTimeoutMs } from '../../../../../utils/cli-timeout.js'; +import { resolveCliTimeoutMs } from '../../../../../utils/cli/cli-timeout.js'; import { emitQueueUpdated, enrichQueueEntries } from '../../../../../utils/queue-enrichment.js'; import { hydrateReplyPreview, type IMessageStore } from '../../stores/ports/MessageStore.js'; import { mergeTokenUsage, type TokenUsage } from '../../types.js'; @@ -1269,6 +1269,7 @@ export class QueueProcessor { verdictPassWarningEnabled: entry.source !== 'connector', // Only policy-backed connector wakes prove a future callback/tracking path. eventDrivenExternalWaitCoverage: entry.eventDrivenExternalWaitCoverage === true, + eventDrivenExternalWaitCoverageKeys: entry.eventDrivenExternalWaitCoverageKeys ?? [], }, )) { if (controller.signal.aborted) { diff --git a/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts b/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts index 416f81099e..b63b8b83cf 100644 --- a/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts +++ b/packages/api/src/domains/cats/services/agents/invocation/invoke-single-cat.ts @@ -69,12 +69,12 @@ import { recordToolUseSpan, } from '../../../../../infrastructure/telemetry/span-helpers.js'; import { ToolSpanTracker } from '../../../../../infrastructure/telemetry/tool-span-tracker.js'; -import { resolveActiveProjectRoot } from '../../../../../utils/active-project-root.js'; -import { resolveCliCommand } from '../../../../../utils/cli-resolve.js'; -import { DEFAULT_CLI_TIMEOUT_MS, resolveCliTimeoutMs } from '../../../../../utils/cli-timeout.js'; -import { findMonorepoRoot, isSameProject } from '../../../../../utils/monorepo-root.js'; -import { isUnderAllowedRoot } from '../../../../../utils/project-path.js'; -import { tcpProbe } from '../../../../../utils/tcp-probe.js'; +import { resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; +import { DEFAULT_CLI_TIMEOUT_MS, resolveCliTimeoutMs } from '../../../../../utils/cli/cli-timeout.js'; +import { tcpProbe } from '../../../../../utils/network/tcp-probe.js'; +import { resolveActiveProjectRoot } from '../../../../../utils/paths/active-project-root.js'; +import { findMonorepoRoot, isSameProject } from '../../../../../utils/paths/monorepo-root.js'; +import { isUnderAllowedRoot } from '../../../../../utils/paths/project-path.js'; import type { AgentPaneRegistry } from '../../../../terminal/agent-pane-registry.js'; import type { TmuxGateway } from '../../../../terminal/tmux-gateway.js'; import { resolveBootcampWorkspaceRoot } from '../../bootcamp/workspace-root.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/ClaudeAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/ClaudeAgentService.ts index 2d735df3de..ee58b08fc9 100644 --- a/packages/api/src/domains/cats/services/agents/providers/ClaudeAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/ClaudeAgentService.ts @@ -22,11 +22,11 @@ import { type CatId, createCatId } from '@cat-cafe/shared'; import { getCatEffort } from '../../../../../config/cat-config-loader.js'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli-diagnostics.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli-resolve.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; +import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli/cli-diagnostics.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; import { CliRawArchive } from '../../session/CliRawArchive.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata } from '../../types.js'; import type { RawArchiveSink } from '../providers/codex-audit-hooks.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/ClaudeBgCarrierService.ts b/packages/api/src/domains/cats/services/agents/providers/ClaudeBgCarrierService.ts index 1f4f2c0784..da32b549e2 100644 --- a/packages/api/src/domains/cats/services/agents/providers/ClaudeBgCarrierService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/ClaudeBgCarrierService.ts @@ -31,8 +31,8 @@ import { isAbsolute, join, resolve } from 'node:path'; import { type CatId, createCatId } from '@cat-cafe/shared'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { resolveCliCommandOrBare } from '../../../../../utils/cli-resolve.js'; -import { buildChildEnv } from '../../../../../utils/cli-spawn.js'; +import { resolveCliCommandOrBare } from '../../../../../utils/cli/cli-resolve.js'; +import { buildChildEnv } from '../../../../../utils/cli/cli-spawn.js'; import type { AgentMessage, AgentService, AgentServiceOptions } from '../../types.js'; import { accumulateUsageFromEntries, diff --git a/packages/api/src/domains/cats/services/agents/providers/CodexAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/CodexAgentService.ts index 50e2cf7f18..52d9425be8 100644 --- a/packages/api/src/domains/cats/services/agents/providers/CodexAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/CodexAgentService.ts @@ -24,11 +24,11 @@ import { getCatModel } from '../../../../../config/cat-models.js'; import { getCodexApprovalPolicy, getCodexSandboxMode } from '../../../../../config/codex-cli.js'; import { estimateCostFromTokens } from '../../../../../config/model-pricing.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli-resolve.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; -import { sanitizeCliStderr } from '../../../../../utils/sanitize-cli-stderr.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; +import { sanitizeCliStderr } from '../../../../../utils/cli/sanitize-cli-stderr.js'; import { AuditEventTypes, getEventAuditLog } from '../../orchestration/EventAuditLog.js'; import { CliRawArchive } from '../../session/CliRawArchive.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata, TokenUsage } from '../../types.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/DareAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/DareAgentService.ts index 7cff6af5d3..2e129dd87c 100644 --- a/packages/api/src/domains/cats/services/agents/providers/DareAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/DareAgentService.ts @@ -20,9 +20,9 @@ import { join } from 'node:path'; import { type CatId, createCatId } from '@cat-cafe/shared'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata } from '../../types.js'; import { transformDareEvent } from './dare-event-transform.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/GeminiAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/GeminiAgentService.ts index 36c3abcdfc..2ed3ef1adf 100644 --- a/packages/api/src/domains/cats/services/agents/providers/GeminiAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/GeminiAgentService.ts @@ -25,9 +25,9 @@ import { basename, join, resolve } from 'node:path'; import { type AgyProfileConfig, type CatId, type CliDiagnostics, createCatId } from '@cat-cafe/shared'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli-diagnostics.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli-resolve.js'; +import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli/cli-diagnostics.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; import { buildChildEnv, isCliError, @@ -35,11 +35,11 @@ import { isCliTimeout, isLivenessWarning, spawnCli, -} from '../../../../../utils/cli-spawn.js'; -import { resolveCliTimeoutMs } from '../../../../../utils/cli-timeout.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; -import { readJsonlTail } from '../../../../../utils/jsonl-tail-reader.js'; -import { sanitizeCliStderr } from '../../../../../utils/sanitize-cli-stderr.js'; +} from '../../../../../utils/cli/cli-spawn.js'; +import { resolveCliTimeoutMs } from '../../../../../utils/cli/cli-timeout.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; +import { sanitizeCliStderr } from '../../../../../utils/cli/sanitize-cli-stderr.js'; +import { readJsonlTail } from '../../../../../utils/parsing/jsonl-tail-reader.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata, TokenUsage } from '../../types.js'; import { appendLocalImagePathHints, collectImageAccessDirectories } from '../providers/image-cli-bridge.js'; import { extractImagePaths } from '../providers/image-paths.js'; @@ -916,7 +916,7 @@ export class GeminiAgentService implements AgentService { invocationId?: string; rawArchivePath?: string; // F212 Phase A (砚砚 2nd P2): cliDiagnostics piggyback on __cliTimeout - cliDiagnostics?: import('../../../../../utils/cli-diagnostics.js').CliDiagnostics; + cliDiagnostics?: import('../../../../../utils/cli/cli-diagnostics.js').CliDiagnostics; } | undefined; let cliErrorEvent: @@ -928,7 +928,7 @@ export class GeminiAgentService implements AgentService { command: string; reasonCode?: string; // F212 Phase A: structured CLI diagnostics piggybacking on __cliError event - cliDiagnostics?: import('../../../../../utils/cli-diagnostics.js').CliDiagnostics; + cliDiagnostics?: import('../../../../../utils/cli/cli-diagnostics.js').CliDiagnostics; } | undefined; let cancelled = false; diff --git a/packages/api/src/domains/cats/services/agents/providers/KimiAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/KimiAgentService.ts index c7c70174c1..35bc37ada6 100644 --- a/packages/api/src/domains/cats/services/agents/providers/KimiAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/KimiAgentService.ts @@ -5,10 +5,10 @@ import { dirname } from 'node:path'; import { type CatId, createCatId } from '@cat-cafe/shared'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli-resolve.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; import { CliRawArchive } from '../../session/CliRawArchive.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata } from '../../types.js'; import type { RawArchiveSink } from '../providers/codex-audit-hooks.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts index c97f524ebc..f2345da33a 100644 --- a/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts @@ -17,11 +17,11 @@ import { type CatId, createCatId } from '@cat-cafe/shared'; import { getCatModel } from '../../../../../config/cat-models.js'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli-diagnostics.js'; -import { formatCliExitError } from '../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli-resolve.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../utils/cli-types.js'; +import { buildCliDiagnostics, buildSilentCompletionDiagnostic } from '../../../../../utils/cli/cli-diagnostics.js'; +import { formatCliExitError } from '../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../utils/cli/cli-resolve.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../utils/cli/cli-types.js'; import { CliRawArchive } from '../../session/CliRawArchive.js'; import type { AgentMessage, AgentServiceOptions, L0InjectableAgentService, MessageMetadata } from '../../types.js'; import type { RawArchiveSink } from '../providers/codex-audit-hooks.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/acp/AcpClient.ts b/packages/api/src/domains/cats/services/agents/providers/acp/AcpClient.ts index 97b650b588..c0291e97ef 100644 --- a/packages/api/src/domains/cats/services/agents/providers/acp/AcpClient.ts +++ b/packages/api/src/domains/cats/services/agents/providers/acp/AcpClient.ts @@ -16,8 +16,8 @@ import { dirname, isAbsolute } from 'node:path'; import { createInterface, type Interface as ReadlineInterface } from 'node:readline'; import { createModuleLogger } from '../../../../../../infrastructure/logger.js'; -import { resolveCliCommandOrBare } from '../../../../../../utils/cli-resolve.js'; -import { resolveWindowsSpawnPlan } from '../../../../../../utils/cli-spawn-win.js'; +import { resolveCliCommandOrBare } from '../../../../../../utils/cli/cli-resolve.js'; +import { resolveWindowsSpawnPlan } from '../../../../../../utils/cli/cli-spawn-win.js'; import type { AcpAgentRequest, AcpContentBlock, diff --git a/packages/api/src/domains/cats/services/agents/providers/acp/AcpHttpStreamClient.ts b/packages/api/src/domains/cats/services/agents/providers/acp/AcpHttpStreamClient.ts index e022594cf9..352d76ebbb 100644 --- a/packages/api/src/domains/cats/services/agents/providers/acp/AcpHttpStreamClient.ts +++ b/packages/api/src/domains/cats/services/agents/providers/acp/AcpHttpStreamClient.ts @@ -23,8 +23,8 @@ import { dirname, isAbsolute } from 'node:path'; import { createInterface } from 'node:readline'; import { createModuleLogger } from '../../../../../../infrastructure/logger.js'; -import { resolveCliCommandOrBare } from '../../../../../../utils/cli-resolve.js'; -import { resolveWindowsSpawnPlan } from '../../../../../../utils/cli-spawn-win.js'; +import { resolveCliCommandOrBare } from '../../../../../../utils/cli/cli-resolve.js'; +import { resolveWindowsSpawnPlan } from '../../../../../../utils/cli/cli-spawn-win.js'; import { type AcpCapacitySignal, type AcpClientConfig, diff --git a/packages/api/src/domains/cats/services/agents/providers/antigravity/antigravity-image-publisher.ts b/packages/api/src/domains/cats/services/agents/providers/antigravity/antigravity-image-publisher.ts index 4d040d3560..9ba7ceffbc 100644 --- a/packages/api/src/domains/cats/services/agents/providers/antigravity/antigravity-image-publisher.ts +++ b/packages/api/src/domains/cats/services/agents/providers/antigravity/antigravity-image-publisher.ts @@ -3,7 +3,7 @@ import { readdir, stat } from 'node:fs/promises'; import { homedir } from 'node:os'; import { extname, join } from 'node:path'; import { createModuleLogger } from '../../../../../../infrastructure/logger.js'; -import { ALLOWED_IMAGE_MIMES, type SupportedImageMime } from '../../../../../../utils/image-storage.js'; +import { ALLOWED_IMAGE_MIMES, type SupportedImageMime } from '../../../../../../utils/media/image-storage.js'; import { type PublishedGeneratedImage, publishGeneratedImage } from '../generated-image-publication.js'; import type { TrajectoryStep } from './AntigravityBridge.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/catagent/openai-chat-adapter.ts b/packages/api/src/domains/cats/services/agents/providers/catagent/openai-chat-adapter.ts index 14ea0ab7ca..3c249b254b 100644 --- a/packages/api/src/domains/cats/services/agents/providers/catagent/openai-chat-adapter.ts +++ b/packages/api/src/domains/cats/services/agents/providers/catagent/openai-chat-adapter.ts @@ -118,7 +118,7 @@ async function* parseOpenAIChatStream( const reader = body.getReader(); const decoder = new TextDecoder('utf-8', { fatal: false }); let buffer = ''; - let dataLines: string[] = []; + const dataLines: string[] = []; const ctx: OpenAIStreamContext = { text: '', textSeen: false, @@ -289,9 +289,7 @@ export class OpenAIChatAdapter implements CatAgentProtocolAdapter { const body: Record = { model: input.model, max_tokens: input.maxTokens ?? DEFAULT_MAX_TOKENS, - messages: input.systemPrompt - ? [{ role: 'system' as const, content: input.systemPrompt }, ...messages] - : messages, + messages: input.systemPrompt ? [{ role: 'system' as const, content: input.systemPrompt }, ...messages] : messages, stream: true, stream_options: { include_usage: true }, }; diff --git a/packages/api/src/domains/cats/services/agents/providers/claude-agent-win.ts b/packages/api/src/domains/cats/services/agents/providers/claude-agent-win.ts index 8a48f8d5ed..ecd67e2180 100644 --- a/packages/api/src/domains/cats/services/agents/providers/claude-agent-win.ts +++ b/packages/api/src/domains/cats/services/agents/providers/claude-agent-win.ts @@ -2,4 +2,4 @@ * Claude Agent Windows Helpers * Re-exports Git Bash detection from shared utility. */ -export { findGitBashPath, pickGitBashPathFromWhere } from '../../../../../utils/cli-spawn-win.js'; +export { findGitBashPath, pickGitBashPathFromWhere } from '../../../../../utils/cli/cli-spawn-win.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/cli-jsonl/CliJsonlAgentService.ts b/packages/api/src/domains/cats/services/agents/providers/cli-jsonl/CliJsonlAgentService.ts index 67f21a576c..6fb3f7c81e 100644 --- a/packages/api/src/domains/cats/services/agents/providers/cli-jsonl/CliJsonlAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/cli-jsonl/CliJsonlAgentService.ts @@ -6,10 +6,10 @@ */ import { type CatId, createCatId } from '@cat-cafe/shared'; -import { formatCliExitError } from '../../../../../../utils/cli-format.js'; -import { formatCliNotFoundError, resolveCliCommand } from '../../../../../../utils/cli-resolve.js'; -import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../../utils/cli-spawn.js'; -import type { SpawnFn } from '../../../../../../utils/cli-types.js'; +import { formatCliExitError } from '../../../../../../utils/cli/cli-format.js'; +import { formatCliNotFoundError, resolveCliCommand } from '../../../../../../utils/cli/cli-resolve.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../../utils/cli/cli-spawn.js'; +import type { SpawnFn } from '../../../../../../utils/cli/cli-types.js'; import { CliRawArchive } from '../../../session/CliRawArchive.js'; import type { AgentMessage, AgentService, AgentServiceOptions, MessageMetadata } from '../../../types.js'; import { type RawArchiveSink, sanitizeRawEvent } from '../codex-audit-hooks.js'; diff --git a/packages/api/src/domains/cats/services/agents/providers/codex-image-scanner.ts b/packages/api/src/domains/cats/services/agents/providers/codex-image-scanner.ts index 1c6fc1f6ac..1225d62cbf 100644 --- a/packages/api/src/domains/cats/services/agents/providers/codex-image-scanner.ts +++ b/packages/api/src/domains/cats/services/agents/providers/codex-image-scanner.ts @@ -1,7 +1,7 @@ import { readdir } from 'node:fs/promises'; import { extname, join } from 'node:path'; import { createModuleLogger } from '../../../../../infrastructure/logger.js'; -import { ALLOWED_IMAGE_MIMES, type SupportedImageMime } from '../../../../../utils/image-storage.js'; +import { ALLOWED_IMAGE_MIMES, type SupportedImageMime } from '../../../../../utils/media/image-storage.js'; import { type PublishedGeneratedImage, publishGeneratedImage } from './generated-image-publication.js'; const log = createModuleLogger('codex-image-scanner'); diff --git a/packages/api/src/domains/cats/services/agents/providers/generated-image-publication.ts b/packages/api/src/domains/cats/services/agents/providers/generated-image-publication.ts index 9ef829b454..dfe7db6d1a 100644 --- a/packages/api/src/domains/cats/services/agents/providers/generated-image-publication.ts +++ b/packages/api/src/domains/cats/services/agents/providers/generated-image-publication.ts @@ -8,8 +8,8 @@ import { type SavedImageAsset, type SupportedImageMime, sanitizeFilenameStem, -} from '../../../../../utils/image-storage.js'; -import { getDefaultUploadDir } from '../../../../../utils/upload-paths.js'; +} from '../../../../../utils/media/image-storage.js'; +import { getDefaultUploadDir } from '../../../../../utils/media/upload-paths.js'; export interface GeneratedImagePublicationInput { sourcePath: string; diff --git a/packages/api/src/domains/cats/services/agents/providers/image-paths.ts b/packages/api/src/domains/cats/services/agents/providers/image-paths.ts index 2980bd672b..44eb0caad1 100644 --- a/packages/api/src/domains/cats/services/agents/providers/image-paths.ts +++ b/packages/api/src/domains/cats/services/agents/providers/image-paths.ts @@ -5,7 +5,7 @@ import { resolve } from 'node:path'; import type { MessageContent } from '@cat-cafe/shared'; -import { getDefaultUploadDir, resolveInternalRouteUrl } from '../../../../../utils/upload-paths.js'; +import { getDefaultUploadDir, resolveInternalRouteUrl } from '../../../../../utils/media/upload-paths.js'; /** * Extract absolute image file paths from contentBlocks. diff --git a/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts b/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts index 6f70c75260..e25c0f3877 100644 --- a/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts +++ b/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts @@ -1502,6 +1502,8 @@ export class AgentRouter { verdictPassWarningEnabled?: boolean; /** Whether event-driven external waits are backed by verified callback/tracking coverage. */ eventDrivenExternalWaitCoverage?: boolean; + /** Canonical external ids covered by that callback/tracking path. */ + eventDrivenExternalWaitCoverageKeys?: readonly string[]; }, ): AsyncIterable { const cleanMessage = stripIntentTags(message); @@ -1624,6 +1626,9 @@ export class AgentRouter { ...(options?.eventDrivenExternalWaitCoverage !== undefined ? { eventDrivenExternalWaitCoverage: options.eventDrivenExternalWaitCoverage } : {}), + ...(options?.eventDrivenExternalWaitCoverageKeys !== undefined + ? { eventDrivenExternalWaitCoverageKeys: options.eventDrivenExternalWaitCoverageKeys } + : {}), }; try { diff --git a/packages/api/src/domains/cats/services/agents/routing/final-routing-slot.ts b/packages/api/src/domains/cats/services/agents/routing/final-routing-slot.ts index c313bb4aa1..094493da05 100644 --- a/packages/api/src/domains/cats/services/agents/routing/final-routing-slot.ts +++ b/packages/api/src/domains/cats/services/agents/routing/final-routing-slot.ts @@ -26,6 +26,8 @@ export interface ValidationInput { readonly rosterHandles: readonly string[]; /** True only when the route has verified callback/EYES coverage for a 2b event-driven wait. */ readonly hasEventDrivenExternalWaitCoverage?: boolean; + /** Canonical external wait ids covered by the verified callback/tracking path. */ + readonly eventDrivenExternalWaitCoverageKeys?: readonly string[]; } export type ValidationResult = @@ -41,6 +43,8 @@ const URL_RE = /https?:\/\/[^\s)\]]+/g; const FENCED_CODE_RE = /```[\s\S]*?```/g; const EVENT_DRIVEN_EXTERNAL_WAIT_RE = /^(?:(?:[-*+]\s+)|(?:\d+[.)]\s+))?External Wait\s*:\s*event-driven\s*\((?!\s*\))[^)\r\n]+\)\s*$/i; +const EVENT_DRIVEN_EXTERNAL_WAIT_ID_RE = + /^(?:(?:[-*+]\s+)|(?:\d+[.)]\s+))?External Wait\s*:\s*event-driven\s*\(([^)\r\n]+)\)\s*$/i; const CAT_SIGNATURE_LINE_RE = /^\s*\[(?:[^[\]\n]+\/[^[\]\n]+|[^[\]\n]+🐾)\]\s*$/u; /** @@ -121,6 +125,54 @@ export function hasEventDrivenExternalWaitExit(text: string | undefined): boolea return slotHasEventDrivenExternalWaitExit(finalRoutingSlotPreservingUrls(stripTrailingCatSignatures(text))); } +function normalizeEventDrivenExternalWaitKey(raw: string | undefined): string | null { + const value = raw?.trim(); + if (!value) return null; + + try { + const url = new URL(value); + if (url.hostname.toLowerCase() === 'github.com') { + const [owner, repo, kind, number] = url.pathname.split('/').filter(Boolean); + if (owner && repo && number && /^\d+$/.test(number)) { + if (kind === 'pull') return `pr:${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`; + if (kind === 'issues') return `issue:${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`; + } + } + } catch { + // Non-URL ids are valid structural ids; normalize below. + } + + return value.toLowerCase(); +} + +export function eventDrivenExternalWaitCoverageKey(raw: string | undefined): string | null { + return normalizeEventDrivenExternalWaitKey(raw); +} + +export function extractEventDrivenExternalWaitKeys(text: string | undefined): string[] { + if (!text) return []; + const slot = finalRoutingSlotPreservingUrls(stripTrailingCatSignatures(text)); + const keys: string[] = []; + for (const line of slot.split(/\r?\n/)) { + const match = line.trim().match(EVENT_DRIVEN_EXTERNAL_WAIT_ID_RE); + const key = normalizeEventDrivenExternalWaitKey(match?.[1]); + if (key) keys.push(key); + } + return keys; +} + +export function hasCoveredEventDrivenExternalWaitExit( + text: string | undefined, + coverageKeys: readonly string[] | undefined, +): boolean { + if (!text || !coverageKeys || coverageKeys.length === 0) return false; + const covered = new Set( + coverageKeys.map((key) => normalizeEventDrivenExternalWaitKey(key)).filter((key): key is string => key !== null), + ); + if (covered.size === 0) return false; + return extractEventDrivenExternalWaitKeys(text).some((key) => covered.has(key)); +} + /** * Find inline @handle mentions in slot (= not at line-start position). * @@ -192,7 +244,12 @@ export function validateRoutingSyntax(input: ValidationInput): ValidationResult if (input.structuredTargetCats.length > 0) return { kind: 'ok' }; const slot = finalRoutingSlot(input.text); - if (input.hasEventDrivenExternalWaitCoverage && hasEventDrivenExternalWaitExit(input.text)) return { kind: 'ok' }; + if ( + input.hasEventDrivenExternalWaitCoverage && + hasCoveredEventDrivenExternalWaitExit(input.text, input.eventDrivenExternalWaitCoverageKeys) + ) { + return { kind: 'ok' }; + } const inlineMentions = findInlineMentionsInSlot(slot, input.rosterHandles); if (inlineMentions.length === 0) return { kind: 'ok' }; diff --git a/packages/api/src/domains/cats/services/agents/routing/guards/routing-guard-remedial.ts b/packages/api/src/domains/cats/services/agents/routing/guards/routing-guard-remedial.ts index 6c7db8ecc9..09e96bf046 100644 --- a/packages/api/src/domains/cats/services/agents/routing/guards/routing-guard-remedial.ts +++ b/packages/api/src/domains/cats/services/agents/routing/guards/routing-guard-remedial.ts @@ -12,7 +12,7 @@ * KD-8 safe:只看"有无机械出口信号",零意图分类器。 */ -import { hasEventDrivenExternalWaitExit } from '../final-routing-slot.js'; +import { hasCoveredEventDrivenExternalWaitExit } from '../final-routing-slot.js'; /** Routing-tool substrings that count as a legitimate exit (持球/群发传球). */ const ROUTING_TOOL_SUBSTRINGS = ['hold_ball', 'multi_mention'] as const; @@ -37,6 +37,8 @@ export interface RoutingExitInput { readonly hasCoCreatorLineStartMention?: boolean; /** True only when the route has verified callback/EYES coverage for a 2b event-driven wait. */ readonly hasEventDrivenExternalWaitCoverage?: boolean; + /** Canonical external wait ids covered by the verified callback/tracking path. */ + readonly eventDrivenExternalWaitCoverageKeys?: readonly string[]; } /** @@ -50,7 +52,12 @@ export function hasValidRoutingExit(input: RoutingExitInput): boolean { if (input.structuredTargetCats.length > 0) return true; if (input.hasCoCreatorLineStartMention) return true; if (hasRoutingToolCall(input.toolNames)) return true; - if (input.hasEventDrivenExternalWaitCoverage && hasEventDrivenExternalWaitExit(input.text)) return true; + if ( + input.hasEventDrivenExternalWaitCoverage && + hasCoveredEventDrivenExternalWaitExit(input.text, input.eventDrivenExternalWaitCoverageKeys) + ) { + return true; + } return false; } diff --git a/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts b/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts index 53fd6a78ed..1a12a378f1 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts @@ -171,6 +171,8 @@ export interface RouteOptions { * Must be true only when the caller has verified callback/tracking coverage for * the external id; text alone does not create a wake-up. */ eventDrivenExternalWaitCoverage?: boolean | undefined; + /** Canonical external ids covered by that callback/tracking path. */ + eventDrivenExternalWaitCoverageKeys?: readonly string[] | undefined; } export interface IncrementalContextResult { diff --git a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts index e3f4242742..1cae912f02 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-serial.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-serial.ts @@ -108,7 +108,8 @@ import { accumulateTextAggregate } from '../text-aggregation.js'; import { formatA2AHandoffContent } from './a2a-handoff-label.js'; import { extractContextEvalSignals } from './context-eval.js'; import { - hasEventDrivenExternalWaitExit, + eventDrivenExternalWaitCoverageKey, + hasCoveredEventDrivenExternalWaitExit, stripTrailingCatSignatures, validateRoutingSyntax, } from './final-routing-slot.js'; @@ -178,7 +179,11 @@ function stripMarkdownRoutePrefix(line: string): string { return line.replace(/^(?:[-*+]\s+|>\s*|\d+[.)]\s+)/, '').trim(); } -function normalizeRouteOnlyRemedialText(text: string, hasEventDrivenExternalWaitCoverage: boolean): string | null { +function normalizeRouteOnlyRemedialText( + text: string, + hasEventDrivenExternalWaitCoverage: boolean, + eventDrivenExternalWaitCoverageKeys: readonly string[], +): string | null { const lines = stripTrailingCatSignatures(text) .trim() .split(/\r?\n/) @@ -187,7 +192,10 @@ function normalizeRouteOnlyRemedialText(text: string, hasEventDrivenExternalWait if (lines.length !== 1) return null; const line = lines[0]!; if (ROUTE_ONLY_REMEDIAL_TEXT_RE.test(line)) return line; - return hasEventDrivenExternalWaitCoverage && hasEventDrivenExternalWaitExit(line) ? line : null; + return hasEventDrivenExternalWaitCoverage && + hasCoveredEventDrivenExternalWaitExit(line, eventDrivenExternalWaitCoverageKeys) + ? line + : null; } function buildRoutingAnalysisContent(storedContent: string, routingContent: string): string { @@ -244,6 +252,35 @@ function isSameTurnEventDrivenCoverageToolName(toolName: string | undefined): bo return normalized === 'register_issue_tracking'; } +function parseToolInputObject(input: unknown): Record | null { + if (!input) return null; + if (typeof input === 'object') return input as Record; + if (typeof input !== 'string') return null; + try { + const parsed = JSON.parse(input); + return parsed && typeof parsed === 'object' ? (parsed as Record) : null; + } catch { + return null; + } +} + +function stringOrNumber(input: unknown): string | null { + if (typeof input === 'string' && input.trim()) return input.trim(); + if (typeof input === 'number' && Number.isFinite(input)) return String(input); + return null; +} + +function eventDrivenCoverageKeysForToolUse(toolName: string | undefined, toolInput: unknown): string[] { + if (!isSameTurnEventDrivenCoverageToolName(toolName)) return []; + const parsed = parseToolInputObject(toolInput); + const repoFullName = stringOrNumber(parsed?.repoFullName); + const issueNumber = stringOrNumber(parsed?.issueNumber); + const key = eventDrivenExternalWaitCoverageKey( + repoFullName && issueNumber ? `issue:${repoFullName}#${issueNumber}` : undefined, + ); + return key ? [key] : []; +} + function isCallbackContentRoutingToolName(toolName: string | undefined): boolean { return isPostMessageToolName(toolName) || isCrossPostMessageToolName(toolName); } @@ -339,24 +376,28 @@ function toolNamesMatch(a: string, b: string): boolean { return a === b || (isPostMessageToolName(a) && isPostMessageToolName(b)); } +type PendingToolResult = { + toolName: string; + eventDrivenExternalWaitCoverageKeys: string[]; +}; + function consumePendingToolResult( - pendingToolResults: string[], + pendingToolResults: PendingToolResult[], msg: AgentMessage, hasConfirmingContent: boolean, hasCallbackPostEvidence: boolean, -): string | undefined { +): PendingToolResult | undefined { const resultToolName = inferToolResultName(msg); if (resultToolName) { - const pendingIndex = pendingToolResults.findIndex((name) => toolNamesMatch(name, resultToolName)); + const pendingIndex = pendingToolResults.findIndex((entry) => toolNamesMatch(entry.toolName, resultToolName)); if (pendingIndex === -1) return undefined; - pendingToolResults.splice(pendingIndex, 1); - return resultToolName; + return pendingToolResults.splice(pendingIndex, 1)[0]; } const firstPending = pendingToolResults[0]; if (!firstPending) return undefined; - if (!isPostMessageToolName(firstPending)) { + if (!isPostMessageToolName(firstPending.toolName)) { return pendingToolResults.shift(); } @@ -404,6 +445,11 @@ export async function* routeSerial( const previousResponses: { catId: CatId; content: string }[] = []; const thinkingMode = options.thinkingMode ?? 'play'; const initialEventDrivenExternalWaitCoverage = options.eventDrivenExternalWaitCoverage === true; + const initialEventDrivenExternalWaitCoverageKeys = initialEventDrivenExternalWaitCoverage + ? (options.eventDrivenExternalWaitCoverageKeys ?? []) + .map((key) => eventDrivenExternalWaitCoverageKey(key)) + .filter((key): key is string => key !== null) + : []; // P2-3 fix: also consider default MCP server path (ClaudeAgentService has fallback resolution) const mcpServerPath = process.env.CAT_CAFE_MCP_SERVER_PATH || resolveDefaultClaudeMcpServerPath(); const incrementalMode = Boolean(currentUserMessageId && deps.deliveryCursorStore); @@ -534,7 +580,18 @@ export async function* routeSerial( const isOriginalTarget = index < targetCats.length; // Event-driven wait coverage proves a wake path for the current invocation target, // not for later A2A worklist entries. - let hasEventDrivenExternalWaitCoverage = initialEventDrivenExternalWaitCoverage && isOriginalTarget; + const eventDrivenExternalWaitCoverageKeys = new Set( + isOriginalTarget ? initialEventDrivenExternalWaitCoverageKeys : [], + ); + let hasEventDrivenExternalWaitCoverage = eventDrivenExternalWaitCoverageKeys.size > 0; + const eventDrivenExternalWaitCoverageKeyList = (): string[] => [...eventDrivenExternalWaitCoverageKeys]; + const addEventDrivenExternalWaitCoverageKeys = (keys: readonly string[]) => { + for (const key of keys) { + const normalized = eventDrivenExternalWaitCoverageKey(key); + if (normalized) eventDrivenExternalWaitCoverageKeys.add(normalized); + } + hasEventDrivenExternalWaitCoverage = eventDrivenExternalWaitCoverageKeys.size > 0; + }; const targetContentBlocks = isOriginalTarget ? routeContentBlocksForCat(catId, contentBlocks) : undefined; const targetUploadDir = targetContentBlocks ? uploadDir : undefined; @@ -923,7 +980,7 @@ export async function* routeSerial( let callbackPostConfirmed = false; let callbackPostMessageId: string | undefined; let awaitingCallbackResult = false; - const pendingToolResults: string[] = []; + const pendingToolResults: PendingToolResult[] = []; const pendingCallbackRoutingExits: CallbackContentRoutingExit[] = []; const confirmedCallbackRoutingMentions = new Set(); let confirmedCallbackRoutingHasCoCreatorLineStartMention = false; @@ -1210,7 +1267,13 @@ export async function* routeSerial( // F148 OQ-2: Collect tool names for context eval if (effectiveMsg.type === 'tool_use' && effectiveMsg.toolName) { collectedToolNames.push(effectiveMsg.toolName); - pendingToolResults.push(effectiveMsg.toolName); + pendingToolResults.push({ + toolName: effectiveMsg.toolName, + eventDrivenExternalWaitCoverageKeys: eventDrivenCoverageKeysForToolUse( + effectiveMsg.toolName, + effectiveMsg.toolInput, + ), + }); const callbackExit = collectCallbackContentRoutingExit( effectiveMsg.toolName, effectiveMsg.toolInput, @@ -1222,7 +1285,7 @@ export async function* routeSerial( // #573: Confirm callback persistence via tool_result success if (effectiveMsg.type === 'tool_result') { const callbackResult = parseCallbackPostResult(effectiveMsg.content); - const completedToolName = consumePendingToolResult( + const completedTool = consumePendingToolResult( pendingToolResults, effectiveMsg, callbackResult.confirmed, @@ -1230,25 +1293,25 @@ export async function* routeSerial( ); if ( awaitingCallbackResult && - completedToolName && - isPostMessageToolName(completedToolName) && + completedTool && + isPostMessageToolName(completedTool.toolName) && callbackResult.confirmed ) { callbackPostConfirmed = true; awaitingCallbackResult = false; if (callbackResult.messageId) callbackPostMessageId = callbackResult.messageId; } - if (completedToolName) { - if (callbackResult.confirmed && isSameTurnEventDrivenCoverageToolName(completedToolName)) { - hasEventDrivenExternalWaitCoverage = true; + if (completedTool) { + if (callbackResult.confirmed && completedTool.eventDrivenExternalWaitCoverageKeys.length > 0) { + addEventDrivenExternalWaitCoverageKeys(completedTool.eventDrivenExternalWaitCoverageKeys); } - settleCallbackRoutingExit(completedToolName, callbackResult.confirmed); + settleCallbackRoutingExit(completedTool.toolName, callbackResult.confirmed); } // F188 Phase F AC-F10 (砚砚 六审 P1-B: also scope by catId for serial route consistency). // 砚砚 cloud-3 P1: also pass toolUseId for exact match when available; // otherwise FIFO toolName+catId match handles same-name parallel calls. - if (deps.toolEventLog && completedToolName) { - const normalizedName = normalizeMcpToolName(completedToolName); + if (deps.toolEventLog && completedTool) { + const normalizedName = normalizeMcpToolName(completedTool.toolName); const resultSummary = deriveResultSummary(normalizedName, effectiveMsg.content); if (Object.keys(resultSummary).length > 0) { const resultMsg = effectiveMsg as { catId?: string; toolUseId?: string }; @@ -1639,7 +1702,13 @@ export async function* routeSerial( } if (effectiveMsg.type === 'tool_use' && effectiveMsg.toolName) { collectedToolNames.push(effectiveMsg.toolName); - pendingToolResults.push(effectiveMsg.toolName); + pendingToolResults.push({ + toolName: effectiveMsg.toolName, + eventDrivenExternalWaitCoverageKeys: eventDrivenCoverageKeysForToolUse( + effectiveMsg.toolName, + effectiveMsg.toolInput, + ), + }); const callbackExit = collectCallbackContentRoutingExit( effectiveMsg.toolName, effectiveMsg.toolInput, @@ -1650,7 +1719,7 @@ export async function* routeSerial( } if (effectiveMsg.type === 'tool_result') { const callbackResult = parseCallbackPostResult(effectiveMsg.content); - const completedToolName = consumePendingToolResult( + const completedTool = consumePendingToolResult( pendingToolResults, effectiveMsg, callbackResult.confirmed, @@ -1658,19 +1727,19 @@ export async function* routeSerial( ); if ( awaitingCallbackResult && - completedToolName && - isPostMessageToolName(completedToolName) && + completedTool && + isPostMessageToolName(completedTool.toolName) && callbackResult.confirmed ) { callbackPostConfirmed = true; awaitingCallbackResult = false; if (callbackResult.messageId) callbackPostMessageId = callbackResult.messageId; } - if (completedToolName) { - if (callbackResult.confirmed && isSameTurnEventDrivenCoverageToolName(completedToolName)) { - hasEventDrivenExternalWaitCoverage = true; + if (completedTool) { + if (callbackResult.confirmed && completedTool.eventDrivenExternalWaitCoverageKeys.length > 0) { + addEventDrivenExternalWaitCoverageKeys(completedTool.eventDrivenExternalWaitCoverageKeys); } - settleCallbackRoutingExit(completedToolName, callbackResult.confirmed); + settleCallbackRoutingExit(completedTool.toolName, callbackResult.confirmed); } } @@ -1690,7 +1759,11 @@ export async function* routeSerial( const remedialExtracted = extractRichFromText(remedialSanitized); const remedialCleanText = remedialExtracted.cleanText; const remedialRouteOnlyContent = remedialCleanText - ? normalizeRouteOnlyRemedialText(remedialCleanText, hasEventDrivenExternalWaitCoverage) + ? normalizeRouteOnlyRemedialText( + remedialCleanText, + hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeyList(), + ) : null; const remedialIsRouteOnly = remedialRouteOnlyContent !== null; // Route-only remedial text (`@cat` / `@co-creator`) is an exit patch, not a replacement artifact. @@ -1772,6 +1845,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: hasRoutingExitCoCreatorLineStartMention(''), hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }) ) { const result = await runRoutingGuardRemedial( @@ -1790,6 +1864,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: result.hasCoCreatorLineStartMention, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }) ) { await appendRoutingGuardFailureNotice(); @@ -1844,6 +1919,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: routingExitHasCoCreatorLineStartMention, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }) ) { const result = await runRoutingGuardRemedial(storedContent, allRichBlocks, [...collectedToolEvents]); @@ -1864,6 +1940,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: routingExitHasCoCreatorLineStartMention, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }) ) { await appendRoutingGuardFailureNotice(); @@ -1901,6 +1978,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], rosterHandles: phaseHRosterHandles, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }); const phaseHHit = phaseHResult.kind === 'invalid_route_syntax'; if (phaseHHit && phaseHResult.kind === 'invalid_route_syntax') { @@ -2056,6 +2134,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: routingExitHasCoCreatorLineStartMention, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }) ) { try { @@ -2119,6 +2198,7 @@ export async function* routeSerial( structuredTargetCats: [...structuredTargetCats], hasCoCreatorLineStartMention: routingExitHasCoCreatorLineStartMention, hasEventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverageKeyList(), }); if (voidHoldEval.shouldEmit) { try { diff --git a/packages/api/src/domains/cats/services/agents/routing/verdict-detect.ts b/packages/api/src/domains/cats/services/agents/routing/verdict-detect.ts index 248adf6098..b6d8fda72e 100644 --- a/packages/api/src/domains/cats/services/agents/routing/verdict-detect.ts +++ b/packages/api/src/domains/cats/services/agents/routing/verdict-detect.ts @@ -13,7 +13,11 @@ * shared-rules §10 已落地,本模块是不依赖猫配合的兜底信号。 */ -import { finalRoutingSlot, hasEventDrivenExternalWaitExit, stripTrailingCatSignatures } from './final-routing-slot.js'; +import { + finalRoutingSlot, + hasCoveredEventDrivenExternalWaitExit, + stripTrailingCatSignatures, +} from './final-routing-slot.js'; /** * Review verdict 关键词。保守集,避免常见日常用语误报: @@ -142,6 +146,8 @@ export interface VerdictWarningInput { readonly hasCoCreatorLineStartMention?: boolean; /** True only when the route has verified callback/EYES coverage for a 2b event-driven wait. */ readonly hasEventDrivenExternalWaitCoverage?: boolean; + /** Canonical external wait ids covered by the verified callback/tracking path. */ + readonly eventDrivenExternalWaitCoverageKeys?: readonly string[]; } /** @@ -160,6 +166,11 @@ export function shouldWarnVerdictWithoutPass(input: VerdictWarningInput): boolea if (hasHoldBallCall(input.toolNames)) return false; if (input.structuredTargetCats.length > 0) return false; if (input.hasCoCreatorLineStartMention) return false; - if (input.hasEventDrivenExternalWaitCoverage && hasEventDrivenExternalWaitExit(input.text)) return false; + if ( + input.hasEventDrivenExternalWaitCoverage && + hasCoveredEventDrivenExternalWaitExit(input.text, input.eventDrivenExternalWaitCoverageKeys) + ) { + return false; + } return true; } diff --git a/packages/api/src/domains/cats/services/agents/routing/void-hold-detect.ts b/packages/api/src/domains/cats/services/agents/routing/void-hold-detect.ts index 66cff59427..0f3ca0aa74 100644 --- a/packages/api/src/domains/cats/services/agents/routing/void-hold-detect.ts +++ b/packages/api/src/domains/cats/services/agents/routing/void-hold-detect.ts @@ -12,7 +12,7 @@ * keyword). `shouldWarnVoidHold` is preserved as a backward-compatible shim. */ -import { hasEventDrivenExternalWaitExit } from './final-routing-slot.js'; +import { hasCoveredEventDrivenExternalWaitExit } from './final-routing-slot.js'; const FENCED_CODE_RE = /```[\s\S]*?```/g; const URL_RE = /https?:\/\/[^\s)\]]+/g; @@ -85,6 +85,8 @@ export interface VoidHoldInput { readonly hasCoCreatorLineStartMention?: boolean; /** True only when the route has verified callback/EYES coverage for a 2b event-driven wait. */ readonly hasEventDrivenExternalWaitCoverage?: boolean; + /** Canonical external wait ids covered by the verified callback/tracking path. */ + readonly eventDrivenExternalWaitCoverageKeys?: readonly string[]; } export interface VoidHoldEvaluation { @@ -112,7 +114,10 @@ export function evaluateVoidHold(input: VoidHoldInput): VoidHoldEvaluation { if (input.lineStartMentions.length > 0) return { shouldEmit: false, matchedPattern: matched }; if (input.structuredTargetCats.length > 0) return { shouldEmit: false, matchedPattern: matched }; if (input.hasCoCreatorLineStartMention) return { shouldEmit: false, matchedPattern: matched }; - if (input.hasEventDrivenExternalWaitCoverage && hasEventDrivenExternalWaitExit(input.text)) { + if ( + input.hasEventDrivenExternalWaitCoverage && + hasCoveredEventDrivenExternalWaitExit(input.text, input.eventDrivenExternalWaitCoverageKeys) + ) { return { shouldEmit: false, matchedPattern: matched }; } return { shouldEmit: true, matchedPattern: matched }; diff --git a/packages/api/src/domains/cats/services/bootcamp/workspace-root.ts b/packages/api/src/domains/cats/services/bootcamp/workspace-root.ts index 01c12b8963..685f8e5dfd 100644 --- a/packages/api/src/domains/cats/services/bootcamp/workspace-root.ts +++ b/packages/api/src/domains/cats/services/bootcamp/workspace-root.ts @@ -1,5 +1,5 @@ import { resolve } from 'node:path'; -import { validateProjectPath } from '../../../../utils/project-path.js'; +import { validateProjectPath } from '../../../../utils/paths/project-path.js'; export type BootcampWorkspaceRootResolution = { ok: true; projectPath: string } | { ok: false; error: string }; diff --git a/packages/api/src/domains/cats/services/context/governance-l0.ts b/packages/api/src/domains/cats/services/context/governance-l0.ts index 82cde52a12..dbd656a688 100644 --- a/packages/api/src/domains/cats/services/context/governance-l0.ts +++ b/packages/api/src/domains/cats/services/context/governance-l0.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { basename, dirname, extname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { findMonorepoRoot } from '../../../../utils/monorepo-root.js'; +import { findMonorepoRoot } from '../../../../utils/paths/monorepo-root.js'; /** * Derive the install root from this module's file path. diff --git a/packages/api/src/domains/cats/services/first-run-quest/client-detection.ts b/packages/api/src/domains/cats/services/first-run-quest/client-detection.ts index 7b5d4bf33e..94710459ce 100644 --- a/packages/api/src/domains/cats/services/first-run-quest/client-detection.ts +++ b/packages/api/src/domains/cats/services/first-run-quest/client-detection.ts @@ -12,7 +12,7 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { resolveCliCommand } from '../../../../utils/cli-resolve.js'; +import { resolveCliCommand } from '../../../../utils/cli/cli-resolve.js'; const execFileAsync = promisify(execFile); diff --git a/packages/api/src/domains/cats/services/types.ts b/packages/api/src/domains/cats/services/types.ts index 1934f91261..a24713a10a 100644 --- a/packages/api/src/domains/cats/services/types.ts +++ b/packages/api/src/domains/cats/services/types.ts @@ -5,8 +5,8 @@ import type { CatId, MessageContent, ReplyPreview, TaskStatus } from '@cat-cafe/shared'; import type { Span } from '@opentelemetry/api'; -import type { CliDiagnostics } from '../../../utils/cli-diagnostics.js'; -import type { CliSpawnOptions } from '../../../utils/cli-types.js'; +import type { CliDiagnostics } from '../../../utils/cli/cli-diagnostics.js'; +import type { CliSpawnOptions } from '../../../utils/cli/cli-types.js'; import type { AntigravitySessionLifecycle } from './agents/providers/antigravity/antigravity-runtime-lifecycle.js'; /** F8: Unified token usage type across all three cats. diff --git a/packages/api/src/domains/plugin/PluginResourceActivator.ts b/packages/api/src/domains/plugin/PluginResourceActivator.ts index ea17317c9f..17789f3827 100644 --- a/packages/api/src/domains/plugin/PluginResourceActivator.ts +++ b/packages/api/src/domains/plugin/PluginResourceActivator.ts @@ -15,7 +15,7 @@ import { readMountRules } from '../../config/mount/mount-rules-store.js'; import type { TaskSpec_P1 } from '../../infrastructure/scheduler/types.js'; import { mountSkillSymlinks, unmountSkillSymlinks } from '../../skills/skill-manage.js'; import { classifyMountPath } from '../../skills/skill-sync-engine.js'; -import { buildSkillMountTargets, isManagedDirectoryLevelSkillsSymlink } from '../../utils/skill-mount.js'; +import { buildSkillMountTargets, isManagedDirectoryLevelSkillsSymlink } from '../../utils/skills/skill-mount.js'; import type { LimbRegistry } from '../limb/LimbRegistry.js'; import { computeAgentProviderDescriptorHash } from './agent-provider-descriptor-hash.js'; import { normalizeCapId, resolvePluginResourcePath, resourceCapId, resourcePathBasename } from './PluginRegistry.js'; diff --git a/packages/api/src/domains/plugin/agent-provider-health-executor.ts b/packages/api/src/domains/plugin/agent-provider-health-executor.ts index 4f1739c143..cbeb078d1b 100644 --- a/packages/api/src/domains/plugin/agent-provider-health-executor.ts +++ b/packages/api/src/domains/plugin/agent-provider-health-executor.ts @@ -32,7 +32,7 @@ import type { AgentProviderHealthResult, PluginAgentProviderResource, } from '@cat-cafe/shared'; -import { resolveCliCommand } from '../../utils/cli-resolve.js'; +import { resolveCliCommand } from '../../utils/cli/cli-resolve.js'; import type { ProviderTransportRegistry } from '../cats/services/agents/providers/transport/ProviderTransportRegistry.js'; /** Inputs to a single health check run. */ diff --git a/packages/api/src/domains/plugin/agent-provider-projection.ts b/packages/api/src/domains/plugin/agent-provider-projection.ts index aa93ab4ceb..0bc9754ef9 100644 --- a/packages/api/src/domains/plugin/agent-provider-projection.ts +++ b/packages/api/src/domains/plugin/agent-provider-projection.ts @@ -22,7 +22,7 @@ * service's exclusive privilege. */ -import type { AgentProviderCapabilityDescriptor, CapabilitiesConfig, CatConfig } from '@cat-cafe/shared'; +import type { AgentProviderCapabilityDescriptor, CapabilitiesConfig, CatColor, CatConfig } from '@cat-cafe/shared'; import { admitForRouting, type RoutingAdmissionCandidate, @@ -75,6 +75,11 @@ export interface AgentProviderProjectionResult { readonly skipped: Array<{ pluginId: string; capId: string; reason: string }>; } +const SYNTHETIC_PROVIDER_COLOR = { + primary: '#334155', + secondary: '#cbd5e1', +} satisfies CatColor; + /** * Build the synthetic CatConfig map. Pure, side-effect free (except onSkip logging). * Caller owns merging the result into `syncAgentRegistry(configs)`. @@ -167,8 +172,8 @@ function synthesizeCatConfig( name: d.name, displayName: d.name, avatar: '🧩', - color: 'gray', - mentionPatterns: [...(binding.mentionPatterns ?? [])], + color: SYNTHETIC_PROVIDER_COLOR, + mentionPatterns: binding.mentionPatterns?.length ? [...binding.mentionPatterns] : [`@${binding.catId}`], clientId: d.name, defaultModel: '', mcpSupport: true, diff --git a/packages/api/src/domains/terminal/tmux-agent-spawner.ts b/packages/api/src/domains/terminal/tmux-agent-spawner.ts index 2d6c25fb3c..0e221faee8 100644 --- a/packages/api/src/domains/terminal/tmux-agent-spawner.ts +++ b/packages/api/src/domains/terminal/tmux-agent-spawner.ts @@ -16,10 +16,10 @@ import type { Interface as ReadlineInterface } from 'node:readline'; import { createInterface } from 'node:readline'; import { promisify } from 'node:util'; import { createModuleLogger } from '../../infrastructure/logger.js'; -import { buildCliDiagnostics } from '../../utils/cli-diagnostics.js'; -import { maybeCollectStreamError } from '../../utils/cli-spawn.js'; -import { resolveCliTimeoutMs } from '../../utils/cli-timeout.js'; -import type { CliSpawnOptions } from '../../utils/cli-types.js'; +import { buildCliDiagnostics } from '../../utils/cli/cli-diagnostics.js'; +import { maybeCollectStreamError } from '../../utils/cli/cli-spawn.js'; +import { resolveCliTimeoutMs } from '../../utils/cli/cli-timeout.js'; +import type { CliSpawnOptions } from '../../utils/cli/cli-types.js'; // parseNDJSON not used directly — we create readline inline for killability. import type { SpawnCliOverride } from '../cats/services/types.js'; import type { AgentPaneRegistry } from './agent-pane-registry.js'; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index d7b03f9b8e..7d5f449b60 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -262,11 +262,11 @@ import { terminalRoutes } from './routes/terminal.js'; import { threadExportRoutes } from './routes/thread-export.js'; import { threadMemberStrategyRoutes } from './routes/thread-member-strategy.js'; import { ApiInstanceLease, type ApiInstanceLeaseInvalidation } from './services/ApiInstanceLease.js'; -import { resolveActiveProjectRoot } from './utils/active-project-root.js'; -import { resolveMemoryRepoPaths } from './utils/memory-root.js'; -import { findMonorepoRoot } from './utils/monorepo-root.js'; +import { getDefaultUploadDir } from './utils/media/upload-paths.js'; +import { resolveActiveProjectRoot } from './utils/paths/active-project-root.js'; +import { resolveMemoryRepoPaths } from './utils/paths/memory-root.js'; +import { findMonorepoRoot } from './utils/paths/monorepo-root.js'; import { resolveUserId } from './utils/request-identity.js'; -import { getDefaultUploadDir } from './utils/upload-paths.js'; const PORT = parseInt(process.env.API_SERVER_PORT ?? '3004', 10); const HOST = process.env.API_SERVER_HOST ?? '127.0.0.1'; @@ -686,7 +686,7 @@ async function main(): Promise { const { resolve } = await import('node:path'); const { repoRoot, docsRoot, markersDir } = resolveMemoryRepoPaths(process.cwd()); - const { initRepoIdentity, isSameRepo } = await import('./utils/is-same-repo.js'); + const { initRepoIdentity, isSameRepo } = await import('./utils/paths/is-same-repo.js'); initRepoIdentity(repoRoot); const { createMemoryServices } = await import('./domains/memory/factory.js'); @@ -3448,7 +3448,7 @@ async function main(): Promise { // F145 P0: Kill orphan agent-browser headless Chrome processes from previous sessions. try { - const { cleanOrphanAgentBrowserChrome } = await import('./utils/orphan-chrome-cleaner.js'); + const { cleanOrphanAgentBrowserChrome } = await import('./utils/process/orphan-chrome-cleaner.js'); await cleanOrphanAgentBrowserChrome(app.log); } catch (err) { app.log.warn(`[api] Orphan Chrome cleanup failed (best-effort): ${String(err)}`); diff --git a/packages/api/src/infrastructure/connectors/ConnectorRouter.ts b/packages/api/src/infrastructure/connectors/ConnectorRouter.ts index 975afc75de..a1b9105000 100644 --- a/packages/api/src/infrastructure/connectors/ConnectorRouter.ts +++ b/packages/api/src/infrastructure/connectors/ConnectorRouter.ts @@ -18,7 +18,7 @@ import type { CatId, ConnectorDefinition, ConnectorSource, MessageContent } from '@cat-cafe/shared'; import { catRegistry, getConnectorDefinition } from '@cat-cafe/shared'; import type { FastifyBaseLogger } from 'fastify'; -import { findMonorepoRoot } from '../../utils/monorepo-root.js'; +import { findMonorepoRoot } from '../../utils/paths/monorepo-root.js'; import type { ConnectorCommandLayer } from './ConnectorCommandLayer.js'; import { type CardAction, ConnectorMessageFormatter, DEFAULT_QUICK_ACTIONS } from './ConnectorMessageFormatter.js'; import type { IConnectorPermissionStore } from './ConnectorPermissionStore.js'; diff --git a/packages/api/src/infrastructure/connectors/OutboundDeliveryHook.ts b/packages/api/src/infrastructure/connectors/OutboundDeliveryHook.ts index ce9fc23800..075c6b2fa7 100644 --- a/packages/api/src/infrastructure/connectors/OutboundDeliveryHook.ts +++ b/packages/api/src/infrastructure/connectors/OutboundDeliveryHook.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { type CatId, catRegistry, type RichBlock } from '@cat-cafe/shared'; import type { FastifyBaseLogger } from 'fastify'; -import { resolveInternalRouteUrl } from '../../utils/upload-paths.js'; +import { resolveInternalRouteUrl } from '../../utils/media/upload-paths.js'; import { ConnectorMessageFormatter, type MessageEnvelope, type MessageOrigin } from './ConnectorMessageFormatter.js'; import type { IConnectorThreadBindingStore } from './ConnectorThreadBindingStore.js'; import { renderAllRichBlocksPlaintext } from './rich-block-plaintext.js'; diff --git a/packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts b/packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts index d8c2d14349..94e7da44bc 100644 --- a/packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts +++ b/packages/api/src/infrastructure/connectors/connector-gateway-bootstrap.ts @@ -28,8 +28,8 @@ import type { RedisClient } from '@cat-cafe/shared/utils'; import type { FastifyBaseLogger } from 'fastify'; import { isCatAvailable } from '../../config/cat-config-loader.js'; import type { ConnectorWebhookHandler } from '../../routes/connector-webhooks.js'; -import { resolveActiveProjectRoot } from '../../utils/active-project-root.js'; -import { getDefaultUploadDir } from '../../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../../utils/media/upload-paths.js'; +import { resolveActiveProjectRoot } from '../../utils/paths/active-project-root.js'; import { encodeDefault } from '../config-field-parser.js'; import { deliverConnectorMessage } from '../email/deliver-connector-message.js'; import { ConnectorCommandLayer, type ConnectorCommandLayerDeps } from './ConnectorCommandLayer.js'; diff --git a/packages/api/src/infrastructure/email/CiCdCheckTaskSpec.ts b/packages/api/src/infrastructure/email/CiCdCheckTaskSpec.ts index 22f7e31c22..087c861c93 100644 --- a/packages/api/src/infrastructure/email/CiCdCheckTaskSpec.ts +++ b/packages/api/src/infrastructure/email/CiCdCheckTaskSpec.ts @@ -80,7 +80,7 @@ export function createCiCdCheckTaskSpec(opts: CiCdCheckTaskSpecOptions): TaskSpe run: { overlap: 'skip', timeoutMs: 30_000, - async execute(signal: CiCdCheckSignal, _subjectKey: string, _ctx: ExecuteContext) { + async execute(signal: CiCdCheckSignal, subjectKey: string, _ctx: ExecuteContext) { const pollResult = await fetchPrStatus(signal.repoFullName, signal.prNumber); if (!pollResult) return; @@ -93,11 +93,13 @@ export function createCiCdCheckTaskSpec(opts: CiCdCheckTaskSpecOptions): TaskSpe // Event-driven wait coverage is stricter: only merge intent guarantees // the follow-up CI-pass transition will invoke this cat again. if (routeResult.bucket === 'fail') { + const eventDrivenExternalWaitCoverage = intent === 'merge'; const policy: ConnectorTriggerPolicy = { priority: 'urgent', reason: 'github_ci_failure', sourceCategory: 'ci', - eventDrivenExternalWaitCoverage: intent === 'merge', + eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverage ? [subjectKey] : [], }; void opts.invokeTrigger .trigger( @@ -131,6 +133,7 @@ export function createCiCdCheckTaskSpec(opts: CiCdCheckTaskSpecOptions): TaskSpe sourceCategory: 'ci', suggestedSkill: 'merge-gate', eventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: [subjectKey], }; void opts.invokeTrigger .trigger( diff --git a/packages/api/src/infrastructure/email/ConnectorInvokeTrigger.ts b/packages/api/src/infrastructure/email/ConnectorInvokeTrigger.ts index 75f701f2c4..2073cbcfdc 100644 --- a/packages/api/src/infrastructure/email/ConnectorInvokeTrigger.ts +++ b/packages/api/src/infrastructure/email/ConnectorInvokeTrigger.ts @@ -61,6 +61,8 @@ export interface ConnectorTriggerPolicy { * messages do not imply 2b event-driven wait coverage. */ readonly eventDrivenExternalWaitCoverage?: boolean; + /** Canonical external ids covered by that callback/tracking path. */ + readonly eventDrivenExternalWaitCoverageKeys?: readonly string[]; /** * Optional queue coalescing key for connector bursts that supersede earlier queued work. * Later hits reuse the first queued entry: messageIds are merged, but the original content/body stays in place. @@ -120,6 +122,7 @@ export class ConnectorInvokeTrigger { const { invocationTracker } = this.opts; const priority = policy?.priority ?? 'normal'; const eventDrivenExternalWaitCoverage = policy?.eventDrivenExternalWaitCoverage === true; + const eventDrivenExternalWaitCoverageKeys = policy?.eventDrivenExternalWaitCoverageKeys ?? []; // F185 AC-1: thread-level queue/processingSlots gate if (this.opts.queueProcessor?.isThreadBusy(threadId)) { @@ -135,6 +138,7 @@ export class ConnectorInvokeTrigger { policy?.suggestedSkill, policy?.coalesceKey, eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys, ); } @@ -153,6 +157,7 @@ export class ConnectorInvokeTrigger { policy?.suggestedSkill, policy?.coalesceKey, eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys, ); } @@ -169,6 +174,7 @@ export class ConnectorInvokeTrigger { sender, controller, eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys, ).catch((err) => { this.opts.log.error(`[ConnectorInvokeTrigger] Unhandled: ${err instanceof Error ? err.message : String(err)}`); }); @@ -187,6 +193,7 @@ export class ConnectorInvokeTrigger { suggestedSkill?: string, coalesceKey?: string, eventDrivenExternalWaitCoverage = false, + eventDrivenExternalWaitCoverageKeys: readonly string[] = [], ): Promise<'full' | 'enqueued'> { const { invocationQueue, socketManager, log } = this.opts; @@ -218,6 +225,7 @@ export class ConnectorInvokeTrigger { ...(sender ? { senderMeta: sender } : {}), ...(suggestedSkill ? { suggestedSkill } : {}), eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: [...eventDrivenExternalWaitCoverageKeys], }); if (result.outcome === 'full') { @@ -275,6 +283,7 @@ export class ConnectorInvokeTrigger { sender?: { id: string; name?: string }, preAcquiredController?: AbortController, eventDrivenExternalWaitCoverage = false, + eventDrivenExternalWaitCoverageKeys: readonly string[] = [], ): Promise { const { router, socketManager, invocationRecordStore, invocationTracker, invocationQueue, log } = this.opts; const targetCats: CatId[] = [catId]; @@ -401,6 +410,7 @@ export class ConnectorInvokeTrigger { verdictPassWarningEnabled: false, // Only policy-backed connector wakes prove a future callback/tracking path. eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys, })) { // #768: Broadcast intent_mode on first CLI event — proves CLI is alive. if (!intentModeBroadcast) { diff --git a/packages/api/src/infrastructure/email/IssueCommentTaskSpec.ts b/packages/api/src/infrastructure/email/IssueCommentTaskSpec.ts index bc0c6baf31..20cfc488a8 100644 --- a/packages/api/src/infrastructure/email/IssueCommentTaskSpec.ts +++ b/packages/api/src/infrastructure/email/IssueCommentTaskSpec.ts @@ -469,11 +469,13 @@ export function createIssueCommentTaskSpec(opts: IssueCommentTaskSpecOptions): T if (opts.invokeTrigger) { try { const coalesceTargetCatId = routeResult.catId || task.ownerCatId || 'unassigned'; + const eventDrivenExternalWaitCoverage = signal.eventDrivenExternalWaitCoverage === true; const policy: ConnectorTriggerPolicy = { priority: 'normal', reason: 'github_issue_comment', sourceCategory: 'issue', - eventDrivenExternalWaitCoverage: signal.eventDrivenExternalWaitCoverage === true, + eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys: eventDrivenExternalWaitCoverage ? [subjectKey] : [], coalesceKey: `${subjectKey}:issue-comment:${coalesceTargetCatId}`, }; void opts.invokeTrigger diff --git a/packages/api/src/infrastructure/email/ReviewFeedbackTaskSpec.ts b/packages/api/src/infrastructure/email/ReviewFeedbackTaskSpec.ts index 6ae7298208..d9f561dc8c 100644 --- a/packages/api/src/infrastructure/email/ReviewFeedbackTaskSpec.ts +++ b/packages/api/src/infrastructure/email/ReviewFeedbackTaskSpec.ts @@ -486,7 +486,8 @@ export function createReviewFeedbackTaskSpec(opts: ReviewFeedbackTaskSpecOptions const suggestedSkill = hasChangesRequested ? 'receive-review' : hasApproved ? 'merge-gate' : undefined; const coalesceTargetCatId = routeResult.catId || task.ownerCatId || 'unassigned'; const intent = task.automationState?.intent ?? 'review'; - const eventDrivenExternalWaitCoverage = hasApproved ? intent === 'merge' : true; + const eventDrivenExternalWaitCoverage = hasApproved && intent === 'merge'; + const eventDrivenExternalWaitCoverageKeys = eventDrivenExternalWaitCoverage ? [subjectKey] : []; const policy: ConnectorTriggerPolicy = { priority: hasChangesRequested ? 'urgent' : 'normal', @@ -494,6 +495,7 @@ export function createReviewFeedbackTaskSpec(opts: ReviewFeedbackTaskSpecOptions sourceCategory: 'review', suggestedSkill, eventDrivenExternalWaitCoverage, + eventDrivenExternalWaitCoverageKeys, coalesceKey: `${subjectKey}:review-feedback:${coalesceTargetCatId}`, }; void opts.invokeTrigger diff --git a/packages/api/src/routes/accounts.ts b/packages/api/src/routes/accounts.ts index 09e885a59d..71056ed74a 100644 --- a/packages/api/src/routes/accounts.ts +++ b/packages/api/src/routes/accounts.ts @@ -16,9 +16,9 @@ import { deleteCatalogAccount, readCatalogAccounts, writeCatalogAccount } from ' import { configEventBus, createChangeSetId } from '../config/config-event-bus.js'; import { deleteCredential, hasCredential, writeCredential } from '../config/credentials.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; -import { findMonorepoRoot } from '../utils/monorepo-root.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; +import { findMonorepoRoot } from '../utils/paths/monorepo-root.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; // clowder-ai#340: Derive client identity from well-known account IDs, not stored protocol. diff --git a/packages/api/src/routes/agent-hooks.ts b/packages/api/src/routes/agent-hooks.ts index dfd9248059..a69dea7b70 100644 --- a/packages/api/src/routes/agent-hooks.ts +++ b/packages/api/src/routes/agent-hooks.ts @@ -1,7 +1,7 @@ import { homedir } from 'node:os'; import type { FastifyPluginAsync, FastifyRequest } from 'fastify'; import { getAgentHookStatus, syncAgentHooks } from '../agent-hooks/index.js'; -import { findMonorepoRoot } from '../utils/monorepo-root.js'; +import { findMonorepoRoot } from '../utils/paths/monorepo-root.js'; export interface AgentHooksRouteOptions { projectRoot?: string; diff --git a/packages/api/src/routes/avatars.ts b/packages/api/src/routes/avatars.ts index 8869bd50d2..3ff1c7a889 100644 --- a/packages/api/src/routes/avatars.ts +++ b/packages/api/src/routes/avatars.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { AVATAR_RAW_FILE_LIMIT_BYTES } from '@cat-cafe/shared'; import multipart from '@fastify/multipart'; import type { FastifyPluginAsync } from 'fastify'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; const ACCEPTED_IMAGE_MIME = ['image/png', 'image/jpeg', 'image/webp'] as const; type AcceptedMime = (typeof ACCEPTED_IMAGE_MIME)[number]; diff --git a/packages/api/src/routes/callback-auth-debug.ts b/packages/api/src/routes/callback-auth-debug.ts index a851fa8117..9f456b911d 100644 --- a/packages/api/src/routes/callback-auth-debug.ts +++ b/packages/api/src/routes/callback-auth-debug.ts @@ -17,7 +17,7 @@ import { createCatId } from '@cat-cafe/shared'; import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { z } from 'zod'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; import type { CallbackAuthSystemMessageNotifier } from './callback-auth-system-message.js'; import { diff --git a/packages/api/src/routes/callback-document-routes.ts b/packages/api/src/routes/callback-document-routes.ts index 7e5ef0938d..012137c51d 100644 --- a/packages/api/src/routes/callback-document-routes.ts +++ b/packages/api/src/routes/callback-document-routes.ts @@ -16,7 +16,7 @@ import type { InvocationRegistry } from '../domains/cats/services/agents/invocat import { getRichBlockBuffer } from '../domains/cats/services/agents/invocation/RichBlockBuffer.js'; import { PandocService } from '../infrastructure/document/PandocService.js'; import type { SocketManager } from '../infrastructure/websocket/index.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; import { requireCallbackAuth } from './callback-auth-prehandler.js'; const generateDocumentSchema = z.object({ diff --git a/packages/api/src/routes/callback-guide-routes.ts b/packages/api/src/routes/callback-guide-routes.ts index 984c9aa808..87202daf2b 100644 --- a/packages/api/src/routes/callback-guide-routes.ts +++ b/packages/api/src/routes/callback-guide-routes.ts @@ -16,7 +16,7 @@ import type { IThreadStore } from '../domains/cats/services/stores/ports/ThreadS import { GuideLifecycleService } from '../domains/guides/GuideLifecycleService.js'; import { createGuideStoreBridge, type IGuideSessionStore } from '../domains/guides/GuideSessionRepository.js'; import type { SocketManager } from '../infrastructure/websocket/index.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { requireCallbackAuth } from './callback-auth-prehandler.js'; // --------------------------------------------------------------------------- diff --git a/packages/api/src/routes/callback-propose-thread-routes.ts b/packages/api/src/routes/callback-propose-thread-routes.ts index 97f9dde98b..63edaf21a8 100644 --- a/packages/api/src/routes/callback-propose-thread-routes.ts +++ b/packages/api/src/routes/callback-propose-thread-routes.ts @@ -19,7 +19,7 @@ import type { IProposalStore } from '../domains/cats/services/stores/ports/Propo import type { IThreadStore } from '../domains/cats/services/stores/ports/ThreadStore.js'; import type { SocketManager } from '../infrastructure/websocket/index.js'; import { normalizeCatIdMentionsInText } from '../utils/cat-mention-handle.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { requireCallbackAuth } from './callback-auth-prehandler.js'; import { buildProposalCardBlock } from './proposal-card-block.js'; diff --git a/packages/api/src/routes/callbacks.ts b/packages/api/src/routes/callbacks.ts index e68b4ade29..fb2537eb24 100644 --- a/packages/api/src/routes/callbacks.ts +++ b/packages/api/src/routes/callbacks.ts @@ -54,7 +54,7 @@ import { buildThreadDeepLink } from '../infrastructure/connectors/connector-comm import { createModuleLogger } from '../infrastructure/logger.js'; import type { SocketManager } from '../infrastructure/websocket/index.js'; import { scoreKeywordRelevance, tokenizeKeyword } from '../utils/keyword-relevance.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; import { getFeatureTagId } from './backlog-doc-import.js'; import { enqueueA2ATargets, triggerA2AInvocation } from './callback-a2a-trigger.js'; import { diff --git a/packages/api/src/routes/capabilities-mcp-write.ts b/packages/api/src/routes/capabilities-mcp-write.ts index 42787262db..07d66b8bf1 100644 --- a/packages/api/src/routes/capabilities-mcp-write.ts +++ b/packages/api/src/routes/capabilities-mcp-write.ts @@ -28,9 +28,9 @@ import { requireLocalCapabilityWriteRequest, resolveCapabilityWriteSessionUserId, } from '../config/capabilities/capability-write-guards.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; -import { resolveMainRepoPath } from '../utils/skill-mount.js'; +import { resolveMainRepoPath } from '../utils/skills/skill-mount.js'; import { type McpProbeResult, probeMcpCapability } from './mcp-probe.js'; const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; diff --git a/packages/api/src/routes/capabilities.ts b/packages/api/src/routes/capabilities.ts index 493152225b..58a78ac18b 100644 --- a/packages/api/src/routes/capabilities.ts +++ b/packages/api/src/routes/capabilities.ts @@ -59,15 +59,15 @@ import { parsePluginManifest } from '../domains/plugin/plugin-manifest.js'; import { parseManifestSkillMeta, readSkillMeta, type SkillMeta } from '../skills/skill-meta.js'; import { syncAll } from '../skills/skill-sync-all.js'; import { type MountConflict, syncProject } from '../skills/skill-sync-engine.js'; -import { pathsEqual, validateProjectPath } from '../utils/project-path.js'; +import { pathsEqual, validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; import { buildMountPointDirCandidates, buildSkillMountTargets, isSkillMountedAtPoint, resolveMainRepoPath, -} from '../utils/skill-mount.js'; -import { resolveCatCafeSkillsSource } from '../utils/skill-source.js'; +} from '../utils/skills/skill-mount.js'; +import { resolveCatCafeSkillsSource } from '../utils/skills/skill-source.js'; import { type McpProbeResult, probeMcpCapability } from './mcp-probe.js'; // ────────── Capability config helpers ────────── diff --git a/packages/api/src/routes/cats.ts b/packages/api/src/routes/cats.ts index 501e2dadf2..c39b18e892 100644 --- a/packages/api/src/routes/cats.ts +++ b/packages/api/src/routes/cats.ts @@ -34,7 +34,7 @@ import { resolveProjectTemplatePath } from '../config/project-template-path.js'; import { getResolvedCats } from '../config/resolved-cats.js'; import { createRuntimeCat, deleteRuntimeCat, updateRuntimeCat } from '../config/runtime-cat-catalog.js'; import { deleteRuntimeOverride, getRuntimeOverride, setRuntimeOverride } from '../config/session-strategy-overrides.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; const colorSchema = z.object({ diff --git a/packages/api/src/routes/config-secrets.ts b/packages/api/src/routes/config-secrets.ts index 8b477f274a..0f31e42b2e 100644 --- a/packages/api/src/routes/config-secrets.ts +++ b/packages/api/src/routes/config-secrets.ts @@ -15,8 +15,8 @@ import { validateConnectorSecretUpdate, } from '../config/connector-secret-write-guards.js'; import { AuditEventTypes, getEventAuditLog } from '../domains/cats/services/orchestration/EventAuditLog.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; const secretsPatchSchema = z.object({ updates: z diff --git a/packages/api/src/routes/config.ts b/packages/api/src/routes/config.ts index b8572ef040..d4f6dde554 100644 --- a/packages/api/src/routes/config.ts +++ b/packages/api/src/routes/config.ts @@ -37,11 +37,11 @@ import { AuditEventTypes, getEventAuditLog } from '../domains/cats/services/orch // Reading process.env.LOG_DIR here would diverge from logger after a runtime // `PATCH /api/config/env` LOG_DIR edit — env-summary would lie about effective path. import { LOG_DIR_PATH } from '../infrastructure/logger.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; import { configCatOrderRoutes } from './config-cat-order.js'; const patchSchema = z.object({ diff --git a/packages/api/src/routes/connector-hub.ts b/packages/api/src/routes/connector-hub.ts index e74ed93dc1..005fc3f83b 100644 --- a/packages/api/src/routes/connector-hub.ts +++ b/packages/api/src/routes/connector-hub.ts @@ -38,7 +38,7 @@ import { } from '../infrastructure/connectors/plugins/im-connector-manifest.js'; import { resolvePluginsDir } from '../infrastructure/connectors/plugins/plugin-installer.js'; import { normalizeTelegramBotToken } from '../infrastructure/connectors/telegram-token.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; const __filename = fileURLToPath(import.meta.url); diff --git a/packages/api/src/routes/connector-plugins.ts b/packages/api/src/routes/connector-plugins.ts index eee66ee5cd..c1054a3dac 100644 --- a/packages/api/src/routes/connector-plugins.ts +++ b/packages/api/src/routes/connector-plugins.ts @@ -33,7 +33,7 @@ import { resolvePluginsDir, uninstallPlugin, } from '../infrastructure/connectors/plugins/plugin-installer.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { resolveSessionUserId } from '../utils/request-identity.js'; import { invalidateManifestCache } from './connector-hub.js'; diff --git a/packages/api/src/routes/first-run-quest.ts b/packages/api/src/routes/first-run-quest.ts index 5ba0fac51f..f61c6060d2 100644 --- a/packages/api/src/routes/first-run-quest.ts +++ b/packages/api/src/routes/first-run-quest.ts @@ -14,9 +14,9 @@ import { z } from 'zod'; import { resolveByAccountRef } from '../config/account-resolver.js'; import { detectAvailableClients } from '../domains/cats/services/first-run-quest/client-detection.js'; import type { FirstRunQuestStateV1, IThreadStore } from '../domains/cats/services/stores/ports/ThreadStore.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; -import { resolveCliCommand } from '../utils/cli-resolve.js'; -import { resolveWindowsSpawnPlan } from '../utils/cli-spawn-win.js'; +import { resolveCliCommand } from '../utils/cli/cli-resolve.js'; +import { resolveWindowsSpawnPlan } from '../utils/cli/cli-spawn-win.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; import { resolveUserId } from '../utils/request-identity.js'; const IS_WINDOWS = process.platform === 'win32'; diff --git a/packages/api/src/routes/governance-status.ts b/packages/api/src/routes/governance-status.ts index 953473588f..89cbf28edb 100644 --- a/packages/api/src/routes/governance-status.ts +++ b/packages/api/src/routes/governance-status.ts @@ -10,8 +10,8 @@ import { join } from 'node:path'; import { promisify } from 'node:util'; import type { FastifyPluginAsync } from 'fastify'; import { checkGovernancePreflight } from '../config/governance/governance-preflight.js'; -import { findMonorepoRoot } from '../utils/monorepo-root.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { findMonorepoRoot } from '../utils/paths/monorepo-root.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; const execFileAsync = promisify(execFile); diff --git a/packages/api/src/routes/image-upload.ts b/packages/api/src/routes/image-upload.ts index b5aee8ad8b..03f704f411 100644 --- a/packages/api/src/routes/image-upload.ts +++ b/packages/api/src/routes/image-upload.ts @@ -4,10 +4,10 @@ */ import { randomUUID } from 'node:crypto'; -import type { SavedImageAsset } from '../utils/image-storage.js'; -import { ImageUploadError, saveImageBufferToUploadDir } from '../utils/image-storage.js'; +import type { SavedImageAsset } from '../utils/media/image-storage.js'; +import { ImageUploadError, saveImageBufferToUploadDir } from '../utils/media/image-storage.js'; -export { ImageUploadError } from '../utils/image-storage.js'; +export { ImageUploadError } from '../utils/media/image-storage.js'; const MAX_FILES = 5; diff --git a/packages/api/src/routes/invocations.ts b/packages/api/src/routes/invocations.ts index 4eefbcdf74..7fe30fdda3 100644 --- a/packages/api/src/routes/invocations.ts +++ b/packages/api/src/routes/invocations.ts @@ -22,7 +22,7 @@ import { parseIntent } from '../domains/cats/services/context/IntentParser.js'; import type { IInvocationRecordStore } from '../domains/cats/services/stores/ports/InvocationRecordStore.js'; import type { IMessageStore } from '../domains/cats/services/stores/ports/MessageStore.js'; import type { SocketManager } from '../infrastructure/websocket/index.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; export interface InvocationsRoutesOptions { invocationRecordStore: IInvocationRecordStore; diff --git a/packages/api/src/routes/messages.ts b/packages/api/src/routes/messages.ts index f7b794b8d6..5660135814 100644 --- a/packages/api/src/routes/messages.ts +++ b/packages/api/src/routes/messages.ts @@ -67,7 +67,7 @@ import { mergeTokenUsage, type TokenUsage } from '../domains/cats/services/types import { buildThreadDeepLink } from '../infrastructure/connectors/connector-command-helpers.js'; import { createModuleLogger } from '../infrastructure/logger.js'; import { buildCancelMessages, type SocketManager } from '../infrastructure/websocket/index.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; /** F088 ISSUE-15: Minimal outbound delivery interface — avoids importing full OutboundDeliveryHook. */ interface OutboundDeliveryHookLike { @@ -97,7 +97,7 @@ interface StreamingHookLike { notifyDeliveryBatchDone?(threadId: string, chainDone: boolean): Promise; } -import { normalizeErrorMessage } from '../utils/normalize-error.js'; +import { normalizeErrorMessage } from '../utils/parsing/normalize-error.js'; import { emitQueueUpdated, enrichQueueEntries } from '../utils/queue-enrichment.js'; import { resolveUserId } from '../utils/request-identity.js'; import { buildGameSeats, parseGameCommand, sanitizeCatIds } from './game-command-interceptor.js'; diff --git a/packages/api/src/routes/mount-rules.ts b/packages/api/src/routes/mount-rules.ts index c96f6968b4..7c2e81f5a4 100644 --- a/packages/api/src/routes/mount-rules.ts +++ b/packages/api/src/routes/mount-rules.ts @@ -30,11 +30,11 @@ import { import { syncAll } from '../skills/skill-sync-all.js'; import { classifyMountPath, syncProject } from '../skills/skill-sync-engine.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; -import { resolvePluginSkillSourcesForProject } from '../utils/plugin-skill-source.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; +import { resolveStartupProjectRoot } from '../utils/paths/startup-root.js'; import { resolveSessionUserId, resolveUserId } from '../utils/request-identity.js'; -import { buildSkillMountTargets, createSkillSymlink, type MountTarget } from '../utils/skill-mount.js'; -import { resolveStartupProjectRoot } from '../utils/startup-root.js'; +import { resolvePluginSkillSourcesForProject } from '../utils/skills/plugin-skill-source.js'; +import { buildSkillMountTargets, createSkillSymlink, type MountTarget } from '../utils/skills/skill-mount.js'; import { resolveSkillsSourceDir } from './skills.js'; const STARTUP_PROJECT_ROOT = resolveStartupProjectRoot(); diff --git a/packages/api/src/routes/plugin-routes.ts b/packages/api/src/routes/plugin-routes.ts index ecbd564eb7..ea0380ae9b 100644 --- a/packages/api/src/routes/plugin-routes.ts +++ b/packages/api/src/routes/plugin-routes.ts @@ -23,7 +23,7 @@ import type { PluginResourceActivator as PluginResourceActivatorType } from '../ import { assertPluginResourceInsideRoot } from '../domains/plugin/PluginResourceActivator.js'; import { loadAllPluginConfigs, resolvePluginEnv, writePluginConfig } from '../domains/plugin/plugin-config-store.js'; import { validateEnvSafety } from '../domains/plugin/plugin-manifest.js'; -import { resolveActiveProjectRoot } from '../utils/active-project-root.js'; +import { resolveActiveProjectRoot } from '../utils/paths/active-project-root.js'; interface PluginRoutesOpts { pluginRegistry: PluginRegistry; diff --git a/packages/api/src/routes/preview.ts b/packages/api/src/routes/preview.ts index bb439db79f..f6be21956b 100644 --- a/packages/api/src/routes/preview.ts +++ b/packages/api/src/routes/preview.ts @@ -5,7 +5,7 @@ import type { FastifyPluginAsync } from 'fastify'; import { AuditEventTypes, type EventAuditLog, getEventAuditLog } from '../domains/cats/services/index.js'; import type { PortDiscoveryService } from '../domains/preview/port-discovery.js'; import { validatePort } from '../domains/preview/port-validator.js'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; interface PreviewRouteOpts { portDiscovery: PortDiscoveryService; diff --git a/packages/api/src/routes/projects-bootstrap.ts b/packages/api/src/routes/projects-bootstrap.ts index 743a08896c..5c1352ed12 100644 --- a/packages/api/src/routes/projects-bootstrap.ts +++ b/packages/api/src/routes/projects-bootstrap.ts @@ -1,7 +1,7 @@ import type { FastifyPluginAsync } from 'fastify'; import type { BootstrapProgress, ExpeditionBootstrapService } from '../domains/memory/ExpeditionBootstrapService.js'; import type { IndexStateManager } from '../domains/memory/IndexStateManager.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; interface SocketManagerLike { diff --git a/packages/api/src/routes/projects-mkdir.ts b/packages/api/src/routes/projects-mkdir.ts index 86397015f8..71cd4dd848 100644 --- a/packages/api/src/routes/projects-mkdir.ts +++ b/packages/api/src/routes/projects-mkdir.ts @@ -7,7 +7,7 @@ import { mkdir, stat } from 'node:fs/promises'; import { basename, join, resolve } from 'node:path'; import type { FastifyPluginAsync } from 'fastify'; -import { isUnderAllowedRoot, validateProjectPath } from '../utils/project-path.js'; +import { isUnderAllowedRoot, validateProjectPath } from '../utils/paths/project-path.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; /** Characters not allowed in directory names (cross-platform safe) */ diff --git a/packages/api/src/routes/projects-setup.ts b/packages/api/src/routes/projects-setup.ts index 404cc4e2be..478fd412e5 100644 --- a/packages/api/src/routes/projects-setup.ts +++ b/packages/api/src/routes/projects-setup.ts @@ -8,8 +8,8 @@ import { execFile } from 'node:child_process'; import { readdir, stat } from 'node:fs/promises'; import { join } from 'node:path'; import type { FastifyPluginAsync } from 'fastify'; -import { findMonorepoRoot } from '../utils/monorepo-root.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { findMonorepoRoot } from '../utils/paths/monorepo-root.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; const VALID_MODES = ['clone', 'init', 'skip'] as const; diff --git a/packages/api/src/routes/projects.ts b/packages/api/src/routes/projects.ts index 47dc8080c3..7a2fe98aa7 100644 --- a/packages/api/src/routes/projects.ts +++ b/packages/api/src/routes/projects.ts @@ -11,7 +11,12 @@ import { homedir } from 'node:os'; import { basename, posix, resolve, win32 } from 'node:path'; import { promisify } from 'node:util'; import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'; -import { getAllowedRoots, isDenylistMode, isUnderAllowedRoot, validateProjectPath } from '../utils/project-path.js'; +import { + getAllowedRoots, + isDenylistMode, + isUnderAllowedRoot, + validateProjectPath, +} from '../utils/paths/project-path.js'; import { resolveHeaderUserId } from '../utils/request-identity.js'; const execFileAsync = promisify(execFile); diff --git a/packages/api/src/routes/proposal-approve-overrides.ts b/packages/api/src/routes/proposal-approve-overrides.ts index 40a4c7c450..1c593a222e 100644 --- a/packages/api/src/routes/proposal-approve-overrides.ts +++ b/packages/api/src/routes/proposal-approve-overrides.ts @@ -11,7 +11,7 @@ import type { CatId, ProposalApproveOverrides, ReportingMode, ThreadProposal } from '@cat-cafe/shared'; import type { IThreadStore } from '../domains/cats/services/stores/ports/ThreadStore.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; /** Parsed approve-body overrides (preferredCats arrives as plain strings from zod). */ export interface ApproveOverridesInput { diff --git a/packages/api/src/routes/push.ts b/packages/api/src/routes/push.ts index 339e26e57f..7e06500674 100644 --- a/packages/api/src/routes/push.ts +++ b/packages/api/src/routes/push.ts @@ -4,7 +4,7 @@ import { requireConnectorWriteOwner, resolveConnectorSessionUserId } from '../co import { AuditEventTypes, getEventAuditLog } from '../domains/cats/services/orchestration/EventAuditLog.js'; import type { PushNotificationService } from '../domains/cats/services/push/PushNotificationService.js'; import type { IPushSubscriptionStore } from '../domains/cats/services/stores/ports/PushSubscriptionStore.js'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; import { describeEndpoint, type PushDeliverySnapshot, diff --git a/packages/api/src/routes/quota.ts b/packages/api/src/routes/quota.ts index a3c8271ee9..4241c2f4d0 100644 --- a/packages/api/src/routes/quota.ts +++ b/packages/api/src/routes/quota.ts @@ -19,7 +19,7 @@ import { promisify } from 'node:util'; import type { FastifyInstance } from 'fastify'; import * as pty from 'node-pty'; import { z } from 'zod'; -import { resolveCliCommand } from '../utils/cli-resolve.js'; +import { resolveCliCommand } from '../utils/cli/cli-resolve.js'; const execFileAsync = promisify(execFile); diff --git a/packages/api/src/routes/ref-audio-upload.ts b/packages/api/src/routes/ref-audio-upload.ts index adcfcbf596..36ad6325c2 100644 --- a/packages/api/src/routes/ref-audio-upload.ts +++ b/packages/api/src/routes/ref-audio-upload.ts @@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import multipart from '@fastify/multipart'; import type { FastifyPluginAsync, FastifyRequest } from 'fastify'; -import { getDefaultUploadDir } from '../utils/upload-paths.js'; +import { getDefaultUploadDir } from '../utils/media/upload-paths.js'; const MAX_REF_AUDIO_BYTES = 10 * 1024 * 1024; diff --git a/packages/api/src/routes/rules.ts b/packages/api/src/routes/rules.ts index 76edb36d7b..6091e1f6b3 100644 --- a/packages/api/src/routes/rules.ts +++ b/packages/api/src/routes/rules.ts @@ -13,7 +13,7 @@ import type { CatCafeConfig } from '@cat-cafe/shared'; import type { FastifyPluginAsync } from 'fastify'; import { getRoster, loadCatConfig, toAllCatConfigs } from '../config/cat-config-loader.js'; import { compileL0ViaSubprocess } from '../domains/cats/services/agents/providers/l0-compiler.js'; -import { getDefaultRootsForPlatform, isPathUnderRoots, validateProjectPath } from '../utils/project-path.js'; +import { getDefaultRootsForPlatform, isPathUnderRoots, validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; function findProjectRoot(): string { diff --git a/packages/api/src/routes/services-lifecycle-helpers.ts b/packages/api/src/routes/services-lifecycle-helpers.ts index dc1508cb35..3d2d794659 100644 --- a/packages/api/src/routes/services-lifecycle-helpers.ts +++ b/packages/api/src/routes/services-lifecycle-helpers.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import type { ServiceLifecycleRunner, ServiceLifecycleRunResult } from '../domains/services/service-lifecycle.js'; import { isValidModelId } from '../domains/services/service-lifecycle.js'; import { MODEL_ENV_VARS, PORT_ENV_VARS } from '../domains/services/service-manifest.js'; -import { isDirectLoopbackRequest } from '../utils/loopback-request.js'; +import { isDirectLoopbackRequest } from '../utils/network/loopback-request.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; export const DEFAULT_LIFECYCLE_TIMEOUT_MS = 30 * 60 * 1000; diff --git a/packages/api/src/routes/skills-drift.ts b/packages/api/src/routes/skills-drift.ts index 58a403f2c3..571c34f8b0 100644 --- a/packages/api/src/routes/skills-drift.ts +++ b/packages/api/src/routes/skills-drift.ts @@ -20,10 +20,10 @@ import { readMountRules } from '../config/mount/mount-rules-store.js'; import { checkGlobal, checkProject } from '../skills/drift-detector.js'; import { syncDrift } from '../skills/drift-resolver.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; -import { pathsEqual, validateProjectPath } from '../utils/project-path.js'; +import { pathsEqual, validateProjectPath } from '../utils/paths/project-path.js'; +import { resolveStartupProjectRoot } from '../utils/paths/startup-root.js'; import { resolveSessionUserId, resolveUserId } from '../utils/request-identity.js'; -import { resolveCatCafeSkillsSource } from '../utils/skill-source.js'; -import { resolveStartupProjectRoot } from '../utils/startup-root.js'; +import { resolveCatCafeSkillsSource } from '../utils/skills/skill-source.js'; const STARTUP_REPO_ROOT = resolveStartupProjectRoot(); diff --git a/packages/api/src/routes/skills-write.ts b/packages/api/src/routes/skills-write.ts index fc635290eb..8be045584d 100644 --- a/packages/api/src/routes/skills-write.ts +++ b/packages/api/src/routes/skills-write.ts @@ -18,11 +18,11 @@ import { validateSkillName } from '../config/governance/skill-sync.js'; import { readMountRules } from '../config/mount/mount-rules-store.js'; import { classifyMountPath, syncProject } from '../skills/skill-sync-engine.js'; import { resolveOwnerGate } from '../utils/owner-gate.js'; -import { resolvePluginSkillSourcesForProject } from '../utils/plugin-skill-source.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveSessionUserId } from '../utils/request-identity.js'; -import { buildSkillMountTargets, createSkillSymlink, resolveMainRepoPath } from '../utils/skill-mount.js'; -import { listSourceSkillNames } from '../utils/skill-source.js'; +import { resolvePluginSkillSourcesForProject } from '../utils/skills/plugin-skill-source.js'; +import { buildSkillMountTargets, createSkillSymlink, resolveMainRepoPath } from '../utils/skills/skill-mount.js'; +import { listSourceSkillNames } from '../utils/skills/skill-source.js'; import { resolveSkillsSourceDir } from './skills.js'; function requireSkillsWriteAccess(request: FastifyRequest, reply: FastifyReply): { error?: string } { diff --git a/packages/api/src/routes/skills.ts b/packages/api/src/routes/skills.ts index ad40cec5d5..9547de483c 100644 --- a/packages/api/src/routes/skills.ts +++ b/packages/api/src/routes/skills.ts @@ -15,15 +15,15 @@ import type { FastifyPluginAsync } from 'fastify'; import { readCapabilitiesConfig } from '../config/capabilities/capability-orchestrator.js'; import { readMountRules } from '../config/mount/mount-rules-store.js'; import { parseManifestSkillMeta, resolveSkillMcpStatuses, type SkillMcpDependency } from '../skills/skill-meta.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; import { buildMountPointDirCandidates, buildSkillMountTargets, isSkillMountedAtPoint, resolveMainRepoPath, -} from '../utils/skill-mount.js'; -import { checkStaleness, listSourceSkillNames, type SkillsStaleness } from '../utils/skill-source.js'; +} from '../utils/skills/skill-mount.js'; +import { checkStaleness, listSourceSkillNames, type SkillsStaleness } from '../utils/skills/skill-source.js'; interface SkillMount { claude: boolean; diff --git a/packages/api/src/routes/threads.ts b/packages/api/src/routes/threads.ts index 7aa9c80b09..4bfc554be7 100644 --- a/packages/api/src/routes/threads.ts +++ b/packages/api/src/routes/threads.ts @@ -34,7 +34,7 @@ import type { ThreadRoutingPolicyV1, } from '../domains/cats/services/stores/ports/ThreadStore.js'; import { createModuleLogger } from '../infrastructure/logger.js'; -import { validateProjectPath } from '../utils/project-path.js'; +import { validateProjectPath } from '../utils/paths/project-path.js'; import { resolveUserId } from '../utils/request-identity.js'; import { getMultiMentionOrchestrator } from './callback-multi-mention-routes.js'; diff --git a/packages/api/src/skills/drift-detector.ts b/packages/api/src/skills/drift-detector.ts index c26ab6dee5..0be9e4a34f 100644 --- a/packages/api/src/skills/drift-detector.ts +++ b/packages/api/src/skills/drift-detector.ts @@ -19,15 +19,15 @@ import { lstat, readdir, readlink, realpath } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { type MountRules, STANDARD_MOUNT_POINT_IDS } from '@cat-cafe/shared'; -import { pathsEqual } from '../utils/project-path.js'; -import { buildSkillMountTargets, isManagedDirectoryLevelSkillsSymlink } from '../utils/skill-mount.js'; +import { pathsEqual } from '../utils/paths/project-path.js'; +import { buildSkillMountTargets, isManagedDirectoryLevelSkillsSymlink } from '../utils/skills/skill-mount.js'; import { canonicalSkillMountPathPolicy, normalizeSkillMountPathPolicy, type SkillMountPathInput, skillAllowsMountPoint, -} from '../utils/skill-mount-policy.js'; -import { listSourceSkillNames } from '../utils/skill-source.js'; +} from '../utils/skills/skill-mount-policy.js'; +import { listSourceSkillNames } from '../utils/skills/skill-source.js'; // ────────── Exported types ────────── diff --git a/packages/api/src/skills/drift-resolver.ts b/packages/api/src/skills/drift-resolver.ts index 1521a5653d..dee60cb927 100644 --- a/packages/api/src/skills/drift-resolver.ts +++ b/packages/api/src/skills/drift-resolver.ts @@ -14,7 +14,7 @@ import { join } from 'node:path'; import type { MountRules } from '@cat-cafe/shared'; import { withCapabilityLock } from '../config/capabilities/capability-orchestrator.js'; import { isValidSkillName } from '../config/governance/skill-sync.js'; -import { buildSkillMountTargets } from '../utils/skill-mount.js'; +import { buildSkillMountTargets } from '../utils/skills/skill-mount.js'; import type { DriftResult } from './drift-detector.js'; import { syncProject } from './skill-sync-engine.js'; diff --git a/packages/api/src/skills/skill-manage.ts b/packages/api/src/skills/skill-manage.ts index f1db813f08..c4390f9bfb 100644 --- a/packages/api/src/skills/skill-manage.ts +++ b/packages/api/src/skills/skill-manage.ts @@ -12,7 +12,7 @@ import { dirname, join, relative } from 'node:path'; import { type CapabilityEntry, type MountRules, STANDARD_MOUNT_POINT_IDS } from '@cat-cafe/shared'; import { readCapabilitiesConfig, writeCapabilitiesConfig } from '../config/capabilities/capability-orchestrator.js'; -import { buildSkillMountTargets, createSkillSymlink } from '../utils/skill-mount.js'; +import { buildSkillMountTargets, createSkillSymlink } from '../utils/skills/skill-mount.js'; import { parseManifestSkillMeta, readSkillMeta } from './skill-meta.js'; import { classifyMountPath, type MountConflict } from './skill-sync-engine.js'; diff --git a/packages/api/src/skills/skill-sync-engine.ts b/packages/api/src/skills/skill-sync-engine.ts index c2f57fab73..035eca65a2 100644 --- a/packages/api/src/skills/skill-sync-engine.ts +++ b/packages/api/src/skills/skill-sync-engine.ts @@ -15,13 +15,13 @@ import { resolveEffectiveSkillMountPaths, validateSkillName, } from '../config/governance/skill-sync.js'; -import { pathsEqual } from '../utils/project-path.js'; +import { pathsEqual } from '../utils/paths/project-path.js'; import { buildSkillMountTargets, createSkillSymlink, isManagedDirectoryLevelSkillsSymlink, -} from '../utils/skill-mount.js'; -import { computeSourceManifestHash, listSourceSkillNames } from '../utils/skill-source.js'; +} from '../utils/skills/skill-mount.js'; +import { computeSourceManifestHash, listSourceSkillNames } from '../utils/skills/skill-source.js'; import { updateConfigAfterSync, writeSkillsSyncState } from './skill-sync-config.js'; function symlinkTargetFor(linkPath: string, sourcePath: string): string { diff --git a/packages/api/src/utils/cli-diagnostics.ts b/packages/api/src/utils/cli/cli-diagnostics.ts similarity index 100% rename from packages/api/src/utils/cli-diagnostics.ts rename to packages/api/src/utils/cli/cli-diagnostics.ts diff --git a/packages/api/src/utils/cli-error-patterns.ts b/packages/api/src/utils/cli/cli-error-patterns.ts similarity index 100% rename from packages/api/src/utils/cli-error-patterns.ts rename to packages/api/src/utils/cli/cli-error-patterns.ts diff --git a/packages/api/src/utils/cli-format.ts b/packages/api/src/utils/cli/cli-format.ts similarity index 100% rename from packages/api/src/utils/cli-format.ts rename to packages/api/src/utils/cli/cli-format.ts diff --git a/packages/api/src/utils/cli-resolve.ts b/packages/api/src/utils/cli/cli-resolve.ts similarity index 100% rename from packages/api/src/utils/cli-resolve.ts rename to packages/api/src/utils/cli/cli-resolve.ts diff --git a/packages/api/src/utils/cli-spawn-win.ts b/packages/api/src/utils/cli/cli-spawn-win.ts similarity index 100% rename from packages/api/src/utils/cli-spawn-win.ts rename to packages/api/src/utils/cli/cli-spawn-win.ts diff --git a/packages/api/src/utils/cli-spawn.ts b/packages/api/src/utils/cli/cli-spawn.ts similarity index 98% rename from packages/api/src/utils/cli-spawn.ts rename to packages/api/src/utils/cli/cli-spawn.ts index 687a3ca8c4..fc1f3eaf46 100644 --- a/packages/api/src/utils/cli-spawn.ts +++ b/packages/api/src/utils/cli/cli-spawn.ts @@ -9,9 +9,11 @@ import { dirname, isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Span } from '@opentelemetry/api'; import { context, SpanStatusCode, trace } from '@opentelemetry/api'; -import { createModuleLogger } from '../infrastructure/logger.js'; -import { registerLivenessProbe, unregisterLivenessProbe } from '../infrastructure/telemetry/instruments.js'; -import { emitOtelLog } from '../infrastructure/telemetry/otel-logger.js'; +import { createModuleLogger } from '../../infrastructure/logger.js'; +import { registerLivenessProbe, unregisterLivenessProbe } from '../../infrastructure/telemetry/instruments.js'; +import { emitOtelLog } from '../../infrastructure/telemetry/otel-logger.js'; +import { isParseError, parseNDJSON } from '../parsing/ndjson-parser.js'; +import { ProcessLivenessProbe } from '../process/ProcessLivenessProbe.js'; import { buildCliDiagnostics, buildCliExitDiagnostic, @@ -23,8 +25,6 @@ import { invalidateCliCommand } from './cli-resolve.js'; import { resolveWindowsSpawnPlan } from './cli-spawn-win.js'; import { resolveCliTimeoutMs } from './cli-timeout.js'; import type { ChildProcessLike, CliSpawnOptions, SpawnFn } from './cli-types.js'; -import { isParseError, parseNDJSON } from './ndjson-parser.js'; -import { ProcessLivenessProbe } from './ProcessLivenessProbe.js'; import { sanitizeCliStderr } from './sanitize-cli-stderr.js'; const log = createModuleLogger('cli-spawn'); @@ -897,7 +897,9 @@ export function isCliTimeout(value: unknown): value is { /** * Type guard for liveness warning events from ProcessLivenessProbe (F118 Phase C) */ -export function isLivenessWarning(value: unknown): value is import('./ProcessLivenessProbe.js').LivenessWarningEvent { +export function isLivenessWarning( + value: unknown, +): value is import('../process/ProcessLivenessProbe.js').LivenessWarningEvent { return ( typeof value === 'object' && value !== null && diff --git a/packages/api/src/utils/cli-supervisor.ts b/packages/api/src/utils/cli/cli-supervisor.ts similarity index 100% rename from packages/api/src/utils/cli-supervisor.ts rename to packages/api/src/utils/cli/cli-supervisor.ts diff --git a/packages/api/src/utils/cli-timeout.ts b/packages/api/src/utils/cli/cli-timeout.ts similarity index 100% rename from packages/api/src/utils/cli-timeout.ts rename to packages/api/src/utils/cli/cli-timeout.ts diff --git a/packages/api/src/utils/cli-types.ts b/packages/api/src/utils/cli/cli-types.ts similarity index 98% rename from packages/api/src/utils/cli-types.ts rename to packages/api/src/utils/cli/cli-types.ts index 2012cf05eb..c35735b09b 100644 --- a/packages/api/src/utils/cli-types.ts +++ b/packages/api/src/utils/cli/cli-types.ts @@ -6,7 +6,7 @@ import type { Readable, Writable } from 'node:stream'; import type { CatId } from '@cat-cafe/shared'; import type { Span } from '@opentelemetry/api'; -import type { AgentMessage } from '../domains/cats/services/types.js'; +import type { AgentMessage } from '../../domains/cats/services/types.js'; /** * Options for spawning a CLI process diff --git a/packages/api/src/utils/sanitize-cli-stderr.ts b/packages/api/src/utils/cli/sanitize-cli-stderr.ts similarity index 100% rename from packages/api/src/utils/sanitize-cli-stderr.ts rename to packages/api/src/utils/cli/sanitize-cli-stderr.ts diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 25224509f8..dac9436e6b 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -3,15 +3,15 @@ * CLI 子进程解析工具导出 */ -export { formatCliExitError } from './cli-format.js'; -export type { CliSpawnerDeps } from './cli-spawn.js'; -export { isCliError, KILL_GRACE_MS, spawnCli } from './cli-spawn.js'; +export { formatCliExitError } from './cli/cli-format.js'; +export type { CliSpawnerDeps } from './cli/cli-spawn.js'; +export { isCliError, KILL_GRACE_MS, spawnCli } from './cli/cli-spawn.js'; export type { ChildProcessLike, CliSpawnOptions, CliTransformer, SpawnFn, -} from './cli-types.js'; -export { isParseError, parseNDJSON } from './ndjson-parser.js'; -export { normalizeErrorMessage } from './normalize-error.js'; -export { isUnderAllowedRoot, validateProjectPath } from './project-path.js'; +} from './cli/cli-types.js'; +export { isParseError, parseNDJSON } from './parsing/ndjson-parser.js'; +export { normalizeErrorMessage } from './parsing/normalize-error.js'; +export { isUnderAllowedRoot, validateProjectPath } from './paths/project-path.js'; diff --git a/packages/api/src/utils/image-storage.ts b/packages/api/src/utils/media/image-storage.ts similarity index 100% rename from packages/api/src/utils/image-storage.ts rename to packages/api/src/utils/media/image-storage.ts diff --git a/packages/api/src/utils/upload-paths.ts b/packages/api/src/utils/media/upload-paths.ts similarity index 93% rename from packages/api/src/utils/upload-paths.ts rename to packages/api/src/utils/media/upload-paths.ts index b70d93bff8..f4e37df618 100644 --- a/packages/api/src/utils/upload-paths.ts +++ b/packages/api/src/utils/media/upload-paths.ts @@ -2,7 +2,7 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const THIS_DIR = dirname(fileURLToPath(import.meta.url)); -const MODULE_DEFAULT_UPLOAD_DIR = resolve(THIS_DIR, '../../uploads'); +const MODULE_DEFAULT_UPLOAD_DIR = resolve(THIS_DIR, '../../../uploads'); /** * Resolve the upload directory. diff --git a/packages/api/src/utils/loopback-request.ts b/packages/api/src/utils/network/loopback-request.ts similarity index 100% rename from packages/api/src/utils/loopback-request.ts rename to packages/api/src/utils/network/loopback-request.ts diff --git a/packages/api/src/utils/tcp-probe.ts b/packages/api/src/utils/network/tcp-probe.ts similarity index 100% rename from packages/api/src/utils/tcp-probe.ts rename to packages/api/src/utils/network/tcp-probe.ts diff --git a/packages/api/src/utils/jsonl-tail-reader.ts b/packages/api/src/utils/parsing/jsonl-tail-reader.ts similarity index 100% rename from packages/api/src/utils/jsonl-tail-reader.ts rename to packages/api/src/utils/parsing/jsonl-tail-reader.ts diff --git a/packages/api/src/utils/ndjson-parser.ts b/packages/api/src/utils/parsing/ndjson-parser.ts similarity index 100% rename from packages/api/src/utils/ndjson-parser.ts rename to packages/api/src/utils/parsing/ndjson-parser.ts diff --git a/packages/api/src/utils/normalize-error.ts b/packages/api/src/utils/parsing/normalize-error.ts similarity index 100% rename from packages/api/src/utils/normalize-error.ts rename to packages/api/src/utils/parsing/normalize-error.ts diff --git a/packages/api/src/utils/active-project-root.ts b/packages/api/src/utils/paths/active-project-root.ts similarity index 100% rename from packages/api/src/utils/active-project-root.ts rename to packages/api/src/utils/paths/active-project-root.ts diff --git a/packages/api/src/utils/is-same-repo.ts b/packages/api/src/utils/paths/is-same-repo.ts similarity index 100% rename from packages/api/src/utils/is-same-repo.ts rename to packages/api/src/utils/paths/is-same-repo.ts diff --git a/packages/api/src/utils/local-override.ts b/packages/api/src/utils/paths/local-override.ts similarity index 100% rename from packages/api/src/utils/local-override.ts rename to packages/api/src/utils/paths/local-override.ts diff --git a/packages/api/src/utils/memory-root.ts b/packages/api/src/utils/paths/memory-root.ts similarity index 100% rename from packages/api/src/utils/memory-root.ts rename to packages/api/src/utils/paths/memory-root.ts diff --git a/packages/api/src/utils/monorepo-root.ts b/packages/api/src/utils/paths/monorepo-root.ts similarity index 100% rename from packages/api/src/utils/monorepo-root.ts rename to packages/api/src/utils/paths/monorepo-root.ts diff --git a/packages/api/src/utils/project-path.ts b/packages/api/src/utils/paths/project-path.ts similarity index 100% rename from packages/api/src/utils/project-path.ts rename to packages/api/src/utils/paths/project-path.ts diff --git a/packages/api/src/utils/startup-root.ts b/packages/api/src/utils/paths/startup-root.ts similarity index 100% rename from packages/api/src/utils/startup-root.ts rename to packages/api/src/utils/paths/startup-root.ts diff --git a/packages/api/src/utils/privileged-route-guard.ts b/packages/api/src/utils/privileged-route-guard.ts index 139149c344..d07912758a 100644 --- a/packages/api/src/utils/privileged-route-guard.ts +++ b/packages/api/src/utils/privileged-route-guard.ts @@ -1,5 +1,5 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; -import { isDirectLoopbackRequest } from './loopback-request.js'; +import { isDirectLoopbackRequest } from './network/loopback-request.js'; import { resolveOwnerGate } from './owner-gate.js'; export type PrivilegedRouteGuardResult = { ok: true; userId: string } | { ok: false; response: { error: string } }; diff --git a/packages/api/src/utils/ProcessLivenessProbe.ts b/packages/api/src/utils/process/ProcessLivenessProbe.ts similarity index 100% rename from packages/api/src/utils/ProcessLivenessProbe.ts rename to packages/api/src/utils/process/ProcessLivenessProbe.ts diff --git a/packages/api/src/utils/orphan-chrome-cleaner.ts b/packages/api/src/utils/process/orphan-chrome-cleaner.ts similarity index 97% rename from packages/api/src/utils/orphan-chrome-cleaner.ts rename to packages/api/src/utils/process/orphan-chrome-cleaner.ts index ae27e7dfe5..8608c8dbf9 100644 --- a/packages/api/src/utils/orphan-chrome-cleaner.ts +++ b/packages/api/src/utils/process/orphan-chrome-cleaner.ts @@ -52,6 +52,7 @@ function isChromeBinary(args: string): boolean { args.startsWith('/Applications/Chromium.app/') || /^\/(?:usr|opt|snap)\S*\/(?:google-chrome|chromium|chrome)/.test(args) || // LL-056 ext: user-local cached Chromium (rod / puppeteer / playwright auto-downloads) + /^\/\S*\/Chromium(?:\s|$)/.test(args) || /^\/\S*\/Chromium\.app\/Contents\/MacOS\/Chromium(?:\s|$)/.test(args) || // LL-056 ext (Linux): Playwright/Puppeteer/Rod cache Chromium in chrome-linux[64]/ subdir /^\/\S*\/chrome-linux(?:64)?\/(?:chrome|headless_shell)(?:\s|$)/.test(args) || @@ -59,7 +60,8 @@ function isChromeBinary(args: string): boolean { /^\/\S*\/chrome-headless-shell(?:\s|$)/.test(args) || // LL-056 ext: cached macOS Chromium helper processes (Renderer/GPU/Network/Plugin). // Scoped to binary-path prefix so prompt text can't false-match. - /\/Chromium\.app\/Contents\/Frameworks\//.test(binaryPath) + /\/Chromium\.app\/Contents\/Frameworks\//.test(binaryPath) || + /\/Chromium Framework\.framework\/.+\/Helpers\/Chromium Helper/.test(binaryPath) ); } diff --git a/packages/api/src/utils/plugin-skill-source.ts b/packages/api/src/utils/skills/plugin-skill-source.ts similarity index 97% rename from packages/api/src/utils/plugin-skill-source.ts rename to packages/api/src/utils/skills/plugin-skill-source.ts index 69d3903d19..30b0c85bfb 100644 --- a/packages/api/src/utils/plugin-skill-source.ts +++ b/packages/api/src/utils/skills/plugin-skill-source.ts @@ -10,8 +10,8 @@ import { existsSync, realpathSync } from 'node:fs'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import type { CapabilitiesConfig } from '@cat-cafe/shared'; -import { resolvePluginResourcePath, resourcePathBasename } from '../domains/plugin/PluginRegistry.js'; -import { parsePluginManifest } from '../domains/plugin/plugin-manifest.js'; +import { resolvePluginResourcePath, resourcePathBasename } from '../../domains/plugin/PluginRegistry.js'; +import { parsePluginManifest } from '../../domains/plugin/plugin-manifest.js'; export interface PluginSkillInfo { pluginId: string; diff --git a/packages/api/src/utils/skill-mount-policy.ts b/packages/api/src/utils/skills/skill-mount-policy.ts similarity index 100% rename from packages/api/src/utils/skill-mount-policy.ts rename to packages/api/src/utils/skills/skill-mount-policy.ts diff --git a/packages/api/src/utils/skill-mount.ts b/packages/api/src/utils/skills/skill-mount.ts similarity index 98% rename from packages/api/src/utils/skill-mount.ts rename to packages/api/src/utils/skills/skill-mount.ts index de143835a0..db11b68744 100644 --- a/packages/api/src/utils/skill-mount.ts +++ b/packages/api/src/utils/skills/skill-mount.ts @@ -1,8 +1,8 @@ import { lstat, readlink, realpath, symlink } from 'node:fs/promises'; import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path'; import { DEFAULT_MOUNT_RULES, type MountRules, STANDARD_MOUNT_POINT_IDS } from '@cat-cafe/shared'; -import { pathsEqual } from './project-path.js'; -import { resolveStartupProjectRoot } from './startup-root.js'; +import { pathsEqual } from '../paths/project-path.js'; +import { resolveStartupProjectRoot } from '../paths/startup-root.js'; export type SkillMountPointKey = 'claude' | 'codex' | 'gemini' | 'kimi'; diff --git a/packages/api/src/utils/skill-source.ts b/packages/api/src/utils/skills/skill-source.ts similarity index 95% rename from packages/api/src/utils/skill-source.ts rename to packages/api/src/utils/skills/skill-source.ts index fd25b2b2f9..53820bdf8e 100644 --- a/packages/api/src/utils/skill-source.ts +++ b/packages/api/src/utils/skills/skill-source.ts @@ -3,8 +3,8 @@ import { existsSync } from 'node:fs'; import { readdir, stat } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { readCapabilitiesConfig } from '../config/capabilities/capability-orchestrator.js'; -import { readSkillsSyncState } from '../skills/skill-sync-config.js'; +import { readCapabilitiesConfig } from '../../config/capabilities/capability-orchestrator.js'; +import { readSkillsSyncState } from '../../skills/skill-sync-config.js'; function resolveCurrentWorktreeSkillsSource(): string { let dir = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/api/test/acp/acp-client-windows-spawn.test.js b/packages/api/test/acp/acp-client-windows-spawn.test.js index effc137e3e..95e9c25963 100644 --- a/packages/api/test/acp/acp-client-windows-spawn.test.js +++ b/packages/api/test/acp/acp-client-windows-spawn.test.js @@ -15,7 +15,7 @@ import { PassThrough } from 'node:stream'; import { afterEach, describe, it, mock } from 'node:test'; const ACP_CLIENT_MODULE = '../../dist/domains/cats/services/agents/providers/acp/AcpClient.js'; -const { findSystemNode } = await import('../../dist/utils/cli-spawn-win.js'); +const { findSystemNode } = await import('../../dist/utils/cli/cli-spawn-win.js'); const require = createRequire(import.meta.url); const childProcess = require('node:child_process'); const INIT_RESULT = { diff --git a/packages/api/test/active-project-root.test.js b/packages/api/test/active-project-root.test.js index 36ad8d8f3c..56f6b131cb 100644 --- a/packages/api/test/active-project-root.test.js +++ b/packages/api/test/active-project-root.test.js @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, it } from 'node:test'; -const { resolveActiveProjectRoot } = await import('../dist/utils/active-project-root.js'); +const { resolveActiveProjectRoot } = await import('../dist/utils/paths/active-project-root.js'); describe('resolveActiveProjectRoot', () => { const savedEnv = {}; diff --git a/packages/api/test/agent-provider-projection.test.js b/packages/api/test/agent-provider-projection.test.js index 4ccdba7b77..bd5c11cbe7 100644 --- a/packages/api/test/agent-provider-projection.test.js +++ b/packages/api/test/agent-provider-projection.test.js @@ -106,6 +106,30 @@ describe('projectRouteableAgentProviders', () => { assert.equal(synth.pluginProjection.pluginId, 'clowder-code'); }); + it('defaults synthetic routeable identity fields for provider configs', () => { + const rows = [ + makeRow({ + overrides: { + descriptor: { + routeableBinding: { + catId: 'clowder-cat', + }, + }, + }, + }), + ]; + const result = projectRouteableAgentProviders({ + rows, + buildSnapshot: () => admittingSnapshot(), + now: () => 30_000, + }); + + const synth = result.configs['clowder-cat']; + assert.ok(synth, 'synthetic config should be projected'); + assert.deepEqual(synth.mentionPatterns, ['@clowder-cat']); + assert.deepEqual(synth.color, { primary: '#334155', secondary: '#cbd5e1' }); + }); + it('skips when health is missing', () => { const rows = [makeRow({ overrides: { descriptor: { health: undefined } } })]; const result = projectRouteableAgentProviders({ diff --git a/packages/api/test/agent-router.test.js b/packages/api/test/agent-router.test.js index d4d6bb1967..50859d728e 100644 --- a/packages/api/test/agent-router.test.js +++ b/packages/api/test/agent-router.test.js @@ -1474,7 +1474,7 @@ describe('AgentRouter', () => { test('passes workingDirectory when thread has non-default projectPath', async () => { const { AgentRouter } = await import('../dist/domains/cats/services/agents/routing/AgentRouter.js'); - const { findMonorepoRoot } = await import('../dist/utils/monorepo-root.js'); + const { findMonorepoRoot } = await import('../dist/utils/paths/monorepo-root.js'); // Keep this path inside the host repo: this test covers workingDirectory // propagation, not the external-project governance gate. const projectPath = findMonorepoRoot(); diff --git a/packages/api/test/capabilities-route.test.js b/packages/api/test/capabilities-route.test.js index cc68a98f4e..1197558459 100644 --- a/packages/api/test/capabilities-route.test.js +++ b/packages/api/test/capabilities-route.test.js @@ -1392,7 +1392,7 @@ describe('GET /api/capabilities (Fastify)', () => { it('realigns stale managed cat-cafe MCP paths to the stable main repo root on GET', async () => { const Fastify = (await import('fastify')).default; const { capabilitiesRoutes } = await import('../dist/routes/capabilities.js'); - const { resolveMainRepoPath } = await import('../dist/utils/skill-mount.js'); + const { resolveMainRepoPath } = await import('../dist/utils/skills/skill-mount.js'); const projectDir = join('/tmp', `cap-route-test-stale-cat-cafe-path-${Date.now()}`); await mkdir(projectDir, { recursive: true }); diff --git a/packages/api/test/cat-config-loader.test.js b/packages/api/test/cat-config-loader.test.js index ac17353273..bb09b81940 100644 --- a/packages/api/test/cat-config-loader.test.js +++ b/packages/api/test/cat-config-loader.test.js @@ -1015,14 +1015,14 @@ describe('F32-b P4c: Sonnet variant in project config', () => { assert.deepEqual(fable.mentionPatterns, ['@fable5', '@fable-5', '@claude-fable-5', '@宪宪5', '@布偶猫5']); }); - it('total cat count is 16 (opus + sonnet + opus-45 + opus-47 + fable-5 + codex + gpt52 + spark + gemini + gemini25 + gemini35 + kimi + dare + antigravity + antig-opus + opencode)', () => { + it('total cat count is 17 (opus + sonnet + opus-45 + opus-47 + fable-5 + codex + gpt52 + spark + gemini + gemini25 + gemini35 + kimi + dare + antigravity + antig-opus + opencode + catagent)', () => { // Use template directly to avoid catalog overlay pollution from earlier tests const templatePath = process.env.CAT_TEMPLATE_PATH ?? resolve(dirname(fileURLToPath(import.meta.url)), '../../..', 'cat-template.json'); const config = loadCatConfig(templatePath); const all = toAllCatConfigs(config); - assert.equal(Object.keys(all).length, 16); + assert.equal(Object.keys(all).length, 17); assert.ok(all.opus); assert.ok(all.sonnet); assert.ok(all['opus-45']); @@ -1039,6 +1039,7 @@ describe('F32-b P4c: Sonnet variant in project config', () => { assert.ok(all.antigravity); // F061: Bengal cat (Antigravity CDP bridge) assert.ok(all['antig-opus']); // F061: Bengal cat Claude variant assert.ok(all.opencode); // F105: OpenCode external agent + assert.ok(all.catagent); // F245: Catagent external provider }); it('antigravity variants have no cli config (F061 Bridge replaces CDP)', () => { diff --git a/packages/api/test/catagent-phase-e.test.js b/packages/api/test/catagent-phase-e.test.js index 742cfb2608..ca68831ad2 100644 --- a/packages/api/test/catagent-phase-e.test.js +++ b/packages/api/test/catagent-phase-e.test.js @@ -106,7 +106,11 @@ function openAITextTurnEvents(text, finishReason = 'stop', promptTokens = 10, co id: `chatcmpl-${Date.now()}`, choices: [{ index: 0, delta: { role: 'assistant', content: text }, finish_reason: finishReason }], }, - { id: `chatcmpl-${Date.now()}`, choices: [], usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens } }, + { + id: `chatcmpl-${Date.now()}`, + choices: [], + usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens }, + }, '[DONE]', ]; } @@ -487,7 +491,10 @@ describe('G2 Axis 5: OpenAI Chat protocol e2e', () => { }); const msgs = await collect(svc.invoke('hi')); - const text = msgs.filter((msg) => msg.type === 'text').map((msg) => msg.content).join(''); + const text = msgs + .filter((msg) => msg.type === 'text') + .map((msg) => msg.content) + .join(''); assert.equal(text, 'Hello from OpenAI'); assert.ok(msgs.some((msg) => msg.type === 'done')); assert.equal(captures[0].url, 'https://proxy.example/v1/chat/completions'); diff --git a/packages/api/test/catagent-protocol-factory.test.js b/packages/api/test/catagent-protocol-factory.test.js index 0c494992ba..42b975b322 100644 --- a/packages/api/test/catagent-protocol-factory.test.js +++ b/packages/api/test/catagent-protocol-factory.test.js @@ -11,13 +11,16 @@ import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; -const { createCatAgentProtocolAdapter, CatAgentProtocolUnknownError } = - await import('../dist/domains/cats/services/agents/providers/catagent/catagent-protocol-factory.js'); +const { createCatAgentProtocolAdapter, CatAgentProtocolUnknownError } = await import( + '../dist/domains/cats/services/agents/providers/catagent/catagent-protocol-factory.js' +); const { AnthropicMessagesAdapter } = await import( '../dist/domains/cats/services/agents/providers/catagent/anthropic-messages-adapter.js' ); -const { OpenAIChatAdapter } = await import('../dist/domains/cats/services/agents/providers/catagent/openai-chat-adapter.js'); +const { OpenAIChatAdapter } = await import( + '../dist/domains/cats/services/agents/providers/catagent/openai-chat-adapter.js' +); describe('createCatAgentProtocolAdapter dispatch (AC-G17 / KD-20 fail-closed)', () => { test('null catConfig → AnthropicMessagesAdapter (legacy/test path)', () => { diff --git a/packages/api/test/catagent-vendor-neutrality.test.js b/packages/api/test/catagent-vendor-neutrality.test.js index b219d4abe3..fc217ec118 100644 --- a/packages/api/test/catagent-vendor-neutrality.test.js +++ b/packages/api/test/catagent-vendor-neutrality.test.js @@ -17,7 +17,18 @@ function stripCommentsAndStrings(source) { test('AC-G12/AC-G27: CatAgentService stays vendor-neutral in code identifiers', () => { const source = readFileSync( - join(__dirname, '..', 'src', 'domains', 'cats', 'services', 'agents', 'providers', 'catagent', 'CatAgentService.ts'), + join( + __dirname, + '..', + 'src', + 'domains', + 'cats', + 'services', + 'agents', + 'providers', + 'catagent', + 'CatAgentService.ts', + ), 'utf-8', ); const stripped = stripCommentsAndStrings(source); diff --git a/packages/api/test/cli-diagnostics-unknown-raw.test.js b/packages/api/test/cli-diagnostics-unknown-raw.test.js index 23254155be..ffebc921c1 100644 --- a/packages/api/test/cli-diagnostics-unknown-raw.test.js +++ b/packages/api/test/cli-diagnostics-unknown-raw.test.js @@ -10,7 +10,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -const { buildCliDiagnostics } = await import('../dist/utils/cli-diagnostics.js'); +const { buildCliDiagnostics } = await import('../dist/utils/cli/cli-diagnostics.js'); const debugRef = { command: 'test-cli', exitCode: 1, exitSignal: undefined, durationMs: 100 }; diff --git a/packages/api/test/cli-diagnostics.test.js b/packages/api/test/cli-diagnostics.test.js index 2913b80121..db08e60738 100644 --- a/packages/api/test/cli-diagnostics.test.js +++ b/packages/api/test/cli-diagnostics.test.js @@ -7,8 +7,8 @@ import { buildCliExitDiagnostic, buildSilentCompletionDiagnostic, formatCliStderrForLog, -} from '../dist/utils/cli-diagnostics.js'; -import { maybeCollectStreamError } from '../dist/utils/cli-spawn.js'; +} from '../dist/utils/cli/cli-diagnostics.js'; +import { maybeCollectStreamError } from '../dist/utils/cli/cli-spawn.js'; const baseRef = { command: 'codex', exitCode: 1, signal: null, invocationId: 'inv-1' }; diff --git a/packages/api/test/cli-error-patterns.test.js b/packages/api/test/cli-error-patterns.test.js index 48864245c8..756e10ede4 100644 --- a/packages/api/test/cli-error-patterns.test.js +++ b/packages/api/test/cli-error-patterns.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert'; import test from 'node:test'; -import { classifyCliError } from '../dist/utils/cli-diagnostics.js'; +import { classifyCliError } from '../dist/utils/cli/cli-diagnostics.js'; const fixtures = [ // Existing (must regress — predates F212, must keep behavior) diff --git a/packages/api/test/cli-resolve.test.js b/packages/api/test/cli-resolve.test.js index 12c4a9d7ba..b85e46703b 100644 --- a/packages/api/test/cli-resolve.test.js +++ b/packages/api/test/cli-resolve.test.js @@ -5,7 +5,7 @@ import { join } from 'node:path'; import test from 'node:test'; const { resolveCliCommand, resolveCliCommandOrBare, formatCliNotFoundError, invalidateCliCommand } = await import( - '../dist/utils/cli-resolve.js' + '../dist/utils/cli/cli-resolve.js' ); // --- formatCliNotFoundError --- diff --git a/packages/api/test/cli-spawn-busy-silent-stall.test.js b/packages/api/test/cli-spawn-busy-silent-stall.test.js index 85d103b181..ff14074bcf 100644 --- a/packages/api/test/cli-spawn-busy-silent-stall.test.js +++ b/packages/api/test/cli-spawn-busy-silent-stall.test.js @@ -17,8 +17,8 @@ import { PassThrough } from 'node:stream'; import { test } from 'node:test'; import { clearTimeout as clearKeepAliveTimeout, setTimeout as setKeepAliveTimeout } from 'node:timers'; -const { spawnCli, isCliTimeout } = await import('../dist/utils/cli-spawn.js'); -const { ProcessLivenessProbe } = await import('../dist/utils/ProcessLivenessProbe.js'); +const { spawnCli, isCliTimeout } = await import('../dist/utils/cli/cli-spawn.js'); +const { ProcessLivenessProbe } = await import('../dist/utils/process/ProcessLivenessProbe.js'); function createMockProcess(opts = {}) { const { exitOnKill = true, exitCode = null, pid = 12345, autoCloseOnExit = true } = opts; @@ -68,7 +68,7 @@ test('stderr handler should NOT call probe.notifyActivity (structural contract)' // Read the cli-spawn source and check whether stderr handler calls notifyActivity const { readFile } = await import('node:fs/promises'); const { join } = await import('node:path'); - const source = await readFile(join(import.meta.dirname, '..', 'src', 'utils', 'cli-spawn.ts'), 'utf-8'); + const source = await readFile(join(import.meta.dirname, '..', 'src', 'utils', 'cli', 'cli-spawn.ts'), 'utf-8'); // Find the stderr handler block using indexOf (regex lazy match stops too early) const stderrIdx = source.indexOf("child.stderr?.on('data'"); @@ -116,7 +116,7 @@ test('stderr handler should NOT call probe.notifyActivity (structural contract)' test('resetTimeout must NOT have early wall-clock cap (pure inactivity timer)', async () => { const { readFile } = await import('node:fs/promises'); const { join } = await import('node:path'); - const source = await readFile(join(import.meta.dirname, '..', 'src', 'utils', 'cli-spawn.ts'), 'utf-8'); + const source = await readFile(join(import.meta.dirname, '..', 'src', 'utils', 'cli', 'cli-spawn.ts'), 'utf-8'); // Find the resetTimeout function body const resetTimeoutMatch = source.match(/const resetTimeout\s*=\s*\(\)\s*(?::\s*void)?\s*=>\s*\{([\s\S]*?)\n {2}\};/); diff --git a/packages/api/test/cli-spawn-native-exe.test.js b/packages/api/test/cli-spawn-native-exe.test.js index b3c4f17236..a5a11e50a7 100644 --- a/packages/api/test/cli-spawn-native-exe.test.js +++ b/packages/api/test/cli-spawn-native-exe.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; -const { shouldDirectSpawnNativeExe } = await import('../dist/utils/cli-spawn-win.js'); +const { shouldDirectSpawnNativeExe } = await import('../dist/utils/cli/cli-spawn-win.js'); describe('shouldDirectSpawnNativeExe', () => { const trueExists = () => true; diff --git a/packages/api/test/cli-spawn-win.test.js b/packages/api/test/cli-spawn-win.test.js index 2c3ef20daa..b0df9d13f2 100644 --- a/packages/api/test/cli-spawn-win.test.js +++ b/packages/api/test/cli-spawn-win.test.js @@ -5,7 +5,7 @@ import { join } from 'node:path'; import test from 'node:test'; const { resolveCmdShimScript, resolveWindowsShimSpawn, escapeCmdArg, extractBareName, parseShimFile, findSystemNode } = - await import('../dist/utils/cli-spawn-win.js'); + await import('../dist/utils/cli/cli-spawn-win.js'); test( 'resolveCmdShimScript supports %dp0 shims and keeps scanning where results until one resolves', diff --git a/packages/api/test/cli-spawn.test.js b/packages/api/test/cli-spawn.test.js index 14f205d620..d87e6f92f3 100644 --- a/packages/api/test/cli-spawn.test.js +++ b/packages/api/test/cli-spawn.test.js @@ -23,10 +23,10 @@ const { KILL_GRACE_MS, SEMANTIC_COMPLETION_GRACE_MS, resolveCliSupervisorNodeArgs, -} = await import('../dist/utils/cli-spawn.js'); -const { DEFAULT_CLI_TIMEOUT_MS } = await import('../dist/utils/cli-timeout.js'); -const { isParseError } = await import('../dist/utils/ndjson-parser.js'); -const { ProcessLivenessProbe } = await import('../dist/utils/ProcessLivenessProbe.js'); +} = await import('../dist/utils/cli/cli-spawn.js'); +const { DEFAULT_CLI_TIMEOUT_MS } = await import('../dist/utils/cli/cli-timeout.js'); +const { isParseError } = await import('../dist/utils/parsing/ndjson-parser.js'); +const { ProcessLivenessProbe } = await import('../dist/utils/process/ProcessLivenessProbe.js'); /** Helper: collect all items from async iterable */ async function collect(iterable) { @@ -281,7 +281,7 @@ test( async () => { const tempDir = await mkdtemp(join(tmpdir(), 'cat-cafe-cli-supervisor-')); const markerPath = join(tempDir, 'terminated.txt'); - const supervisorPath = fileURLToPath(new URL('../dist/utils/cli-supervisor.js', import.meta.url)); + const supervisorPath = fileURLToPath(new URL('../dist/utils/cli/cli-supervisor.js', import.meta.url)); const childScript = [ 'const fs = require("node:fs");', `process.on("SIGTERM", () => { fs.writeFileSync(${JSON.stringify(markerPath)}, "SIGTERM"); process.exit(0); });`, @@ -329,7 +329,7 @@ test( 'cli supervisor escalates stubborn supervised child before parent kill grace elapses', { skip: process.platform === 'win32' && 'Unix process-group supervisor is not used on Windows' }, async () => { - const supervisorPath = fileURLToPath(new URL('../dist/utils/cli-supervisor.js', import.meta.url)); + const supervisorPath = fileURLToPath(new URL('../dist/utils/cli/cli-supervisor.js', import.meta.url)); const childScript = ['process.on("SIGTERM", () => {});', 'setInterval(() => {}, 60_000);'].join('\n'); const supervisor = nodeSpawn(process.execPath, [supervisorPath, '--', process.execPath, '-e', childScript], { env: { @@ -858,7 +858,7 @@ test('spawnCli marks no rollout found stderr as missing_rollout reasonCode', asy }); test('formatCliExitError propagates reasonCode into message string', async () => { - const { formatCliExitError } = await import('../dist/utils/cli-format.js'); + const { formatCliExitError } = await import('../dist/utils/cli/cli-format.js'); // Without reasonCode — unchanged behavior assert.equal( @@ -1801,7 +1801,7 @@ test('F212 (云端 codex P2): maybeCollectStreamError extracts Error.message (no // serializes correctly even without the fix. The real bug surfaces only when `evt.error` is a // genuine Error instance — name/message/stack are non-enumerable on Error.prototype and would // serialize to `{}`. Test the pure helper directly to exercise the actual failure mode. - const { maybeCollectStreamError } = await import('../dist/utils/cli-spawn.js'); + const { maybeCollectStreamError } = await import('../dist/utils/cli/cli-spawn.js'); const sink = []; const evt = { type: 'error', error: new Error('401 Unauthorized') }; maybeCollectStreamError(evt, sink); @@ -1811,7 +1811,7 @@ test('F212 (云端 codex P2): maybeCollectStreamError extracts Error.message (no `Error.message must survive non-enumerable serialization; got: ${sink[0]}`, ); // Verify the classifier can actually reach the reasonCode from the extracted text - const { classifyCliError } = await import('../dist/utils/cli-diagnostics.js'); + const { classifyCliError } = await import('../dist/utils/cli/cli-diagnostics.js'); assert.equal( classifyCliError(sink[0]), 'auth_failed', @@ -1821,7 +1821,7 @@ test('F212 (云端 codex P2): maybeCollectStreamError extracts Error.message (no test('F212 (云端 codex P2): maybeCollectStreamError extracts plain object error fields', async () => { // Regression: plain {error:{name,message,data:{message,statusCode}}} should also work. - const { maybeCollectStreamError } = await import('../dist/utils/cli-spawn.js'); + const { maybeCollectStreamError } = await import('../dist/utils/cli/cli-spawn.js'); const sink = []; maybeCollectStreamError( { @@ -1836,7 +1836,7 @@ test('F212 (云端 codex P2): maybeCollectStreamError extracts plain object erro }); test('F212 (云端 codex P2): maybeCollectStreamError ignores non-error events', async () => { - const { maybeCollectStreamError } = await import('../dist/utils/cli-spawn.js'); + const { maybeCollectStreamError } = await import('../dist/utils/cli/cli-spawn.js'); const sink = []; maybeCollectStreamError({ type: 'text', content: 'hello' }, sink); maybeCollectStreamError(null, sink); @@ -1847,7 +1847,7 @@ test('F212 (云端 codex P2): maybeCollectStreamError ignores non-error events', test('F212 (云端 codex round-5 P2): maybeCollectStreamError bounds sink entries', async () => { // Long-running session emitting many error events must not grow sink unbounded. - const { maybeCollectStreamError } = await import('../dist/utils/cli-spawn.js'); + const { maybeCollectStreamError } = await import('../dist/utils/cli/cli-spawn.js'); const sink = []; // Push 100 small error events; cap is 50 entries. for (let i = 0; i < 100; i++) { @@ -1858,7 +1858,7 @@ test('F212 (云端 codex round-5 P2): maybeCollectStreamError bounds sink entrie test('F212 (云端 codex round-5 P2): maybeCollectStreamError bounds total chars', async () => { // Single huge error event must not push beyond 16384 chars total. - const { maybeCollectStreamError } = await import('../dist/utils/cli-spawn.js'); + const { maybeCollectStreamError } = await import('../dist/utils/cli/cli-spawn.js'); const sink = []; const huge = 'A'.repeat(20000); // 20KB single message maybeCollectStreamError({ type: 'error', error: { message: huge } }, sink); diff --git a/packages/api/test/cli-timeout.test.js b/packages/api/test/cli-timeout.test.js index 715a59561f..1479124d3c 100644 --- a/packages/api/test/cli-timeout.test.js +++ b/packages/api/test/cli-timeout.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; const { DEFAULT_CLI_TIMEOUT_MS, parseCliTimeoutMs, readCliTimeoutMsFromEnv, resolveCliTimeoutMs } = await import( - '../dist/utils/cli-timeout.js' + '../dist/utils/cli/cli-timeout.js' ); describe('cli-timeout', () => { diff --git a/packages/api/test/connector-invoke-trigger.test.js b/packages/api/test/connector-invoke-trigger.test.js index 9a408d22de..95f9f13e08 100644 --- a/packages/api/test/connector-invoke-trigger.test.js +++ b/packages/api/test/connector-invoke-trigger.test.js @@ -297,12 +297,14 @@ describe('ConnectorInvokeTrigger', () => { reason: 'github_review_feedback', sourceCategory: 'review', eventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:owner/repo#1'], }, ); await waitForTrigger(); assert.strictEqual(routerMock.calls.length, 1); assert.strictEqual(routerMock.calls[0].options?.eventDrivenExternalWaitCoverage, true); + assert.deepStrictEqual(routerMock.calls[0].options?.eventDrivenExternalWaitCoverageKeys, ['pr:owner/repo#1']); }); it('broadcasts agent messages to WebSocket room', async () => { @@ -1158,6 +1160,7 @@ describe('ConnectorInvokeTrigger', () => { reason: 'github_review_feedback', sourceCategory: 'review', eventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:owner/repo#1'], }, ); await waitForTrigger(); @@ -1165,6 +1168,7 @@ describe('ConnectorInvokeTrigger', () => { const entries = queue.list('thread-1', 'user-1'); assert.strictEqual(entries.length, 1); assert.strictEqual(entries[0].eventDrivenExternalWaitCoverage, true); + assert.deepStrictEqual(entries[0].eventDrivenExternalWaitCoverageKeys, ['pr:owner/repo#1']); }); it('urgent connector with owner mismatch still enqueues without cancel (F175)', async () => { diff --git a/packages/api/test/final-routing-slot.test.js b/packages/api/test/final-routing-slot.test.js index 809504b971..b03cc77fa1 100644 --- a/packages/api/test/final-routing-slot.test.js +++ b/packages/api/test/final-routing-slot.test.js @@ -161,6 +161,7 @@ describe('F167 Phase H AC-H3: validateRoutingSyntax trigger conditions', () => { structuredTargetCats: [], rosterHandles: roster, hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:35'], }); assert.equal(result.kind, 'ok'); }); @@ -177,6 +178,7 @@ describe('F167 Phase H AC-H3: validateRoutingSyntax trigger conditions', () => { structuredTargetCats: [], rosterHandles: roster, hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:35'], }); assert.equal(result.kind, 'ok'); }); @@ -194,6 +196,7 @@ describe('F167 Phase H AC-H3: validateRoutingSyntax trigger conditions', () => { structuredTargetCats: [], rosterHandles: roster, hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:clowder-labs/clowder-ai#35'], }); assert.equal(result.kind, 'ok'); }); diff --git a/packages/api/test/governance/skills-state.test.js b/packages/api/test/governance/skills-state.test.js index 8fc9fd3187..cf6cc40c50 100644 --- a/packages/api/test/governance/skills-state.test.js +++ b/packages/api/test/governance/skills-state.test.js @@ -17,7 +17,11 @@ import { updateSkillMountPaths, writeSkillsSyncState, } from '../../dist/skills/skill-sync-config.js'; -import { checkStaleness, computeSourceManifestHash, listSourceSkillNames } from '../../dist/utils/skill-source.js'; +import { + checkStaleness, + computeSourceManifestHash, + listSourceSkillNames, +} from '../../dist/utils/skills/skill-source.js'; let tempDir; diff --git a/packages/api/test/image-storage.test.js b/packages/api/test/image-storage.test.js index b382dfd709..46983e3554 100644 --- a/packages/api/test/image-storage.test.js +++ b/packages/api/test/image-storage.test.js @@ -19,7 +19,7 @@ describe('image-storage', () => { }); it('saves a validated image buffer to uploadDir and returns /uploads metadata', async () => { - const { saveImageBufferToUploadDir } = await import('../dist/utils/image-storage.js'); + const { saveImageBufferToUploadDir } = await import('../dist/utils/media/image-storage.js'); const result = await saveImageBufferToUploadDir({ buffer: Buffer.from('fake-png'), @@ -38,7 +38,7 @@ describe('image-storage', () => { }); it('copies a local image file into uploadDir and returns canonical /uploads metadata', async () => { - const { copyImageFileToUploadDir } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir } = await import('../dist/utils/media/image-storage.js'); const sourcePath = join(sourceDir, 'source.png'); await writeFile(sourcePath, Buffer.from('source-png')); @@ -59,7 +59,7 @@ describe('image-storage', () => { }); it('rejects copied image files exceeding 10MB', async () => { - const { copyImageFileToUploadDir, ImageUploadError } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir, ImageUploadError } = await import('../dist/utils/media/image-storage.js'); const sourcePath = join(sourceDir, 'too-large.png'); await writeFile(sourcePath, Buffer.alloc(10 * 1024 * 1024 + 1)); @@ -78,7 +78,7 @@ describe('image-storage', () => { it('throws on duplicate target when onExists is error', async () => { const { copyImageFileToUploadDir, saveImageBufferToUploadDir, ImageUploadError } = await import( - '../dist/utils/image-storage.js' + '../dist/utils/media/image-storage.js' ); const sourcePath = join(sourceDir, 'source.png'); @@ -125,7 +125,7 @@ describe('image-storage', () => { }); it('reuses duplicate target when onExists is reuse', async () => { - const { copyImageFileToUploadDir } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir } = await import('../dist/utils/media/image-storage.js'); const sourcePath = join(sourceDir, 'source.png'); await writeFile(sourcePath, Buffer.from('source-png')); @@ -151,7 +151,7 @@ describe('image-storage', () => { }); it('honors onExists when source path already equals the target path', async () => { - const { copyImageFileToUploadDir, ImageUploadError } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir, ImageUploadError } = await import('../dist/utils/media/image-storage.js'); const sourcePath = join(uploadDir, 'same-path.png'); await writeFile(sourcePath, Buffer.from('already-published')); @@ -181,7 +181,7 @@ describe('image-storage', () => { }); it('rejects missing same-path source instead of returning phantom metadata', async () => { - const { copyImageFileToUploadDir } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir } = await import('../dist/utils/media/image-storage.js'); const missingSourcePath = join(uploadDir, 'missing-same-path.png'); @@ -199,7 +199,7 @@ describe('image-storage', () => { }); it('reuses an existing target when original source has been cleaned up', async () => { - const { copyImageFileToUploadDir } = await import('../dist/utils/image-storage.js'); + const { copyImageFileToUploadDir } = await import('../dist/utils/media/image-storage.js'); const existingTargetPath = join(uploadDir, 'recovered-image.png'); const missingSourcePath = join(sourceDir, 'already-cleaned-up.png'); diff --git a/packages/api/test/image-upload.test.js b/packages/api/test/image-upload.test.js index 87e5758678..67f044b42b 100644 --- a/packages/api/test/image-upload.test.js +++ b/packages/api/test/image-upload.test.js @@ -113,7 +113,7 @@ describe('saveUploadedImages', () => { describe('extractImagePaths', () => { it('uses packages/api/uploads as the default upload dir regardless of cwd', async () => { - const { getDefaultUploadDir } = await import('../dist/utils/upload-paths.js'); + const { getDefaultUploadDir } = await import('../dist/utils/media/upload-paths.js'); const dir = getDefaultUploadDir(); assert.ok( diff --git a/packages/api/test/integration/thread-wiring.test.js b/packages/api/test/integration/thread-wiring.test.js index 6d567a5ded..90c8e80378 100644 --- a/packages/api/test/integration/thread-wiring.test.js +++ b/packages/api/test/integration/thread-wiring.test.js @@ -25,7 +25,7 @@ const { MessageStore } = await import('../../dist/domains/cats/services/stores/p const { ThreadStore } = await import('../../dist/domains/cats/services/stores/ports/ThreadStore.js'); const { threadsRoutes } = await import('../../dist/routes/threads.js'); const { messagesRoutes } = await import('../../dist/routes/messages.js'); -const { findMonorepoRoot } = await import('../../dist/utils/monorepo-root.js'); +const { findMonorepoRoot } = await import('../../dist/utils/paths/monorepo-root.js'); // --- Helpers --- diff --git a/packages/api/test/jsonl-tail-reader.test.js b/packages/api/test/jsonl-tail-reader.test.js index af355081fc..d7e87f8bf4 100644 --- a/packages/api/test/jsonl-tail-reader.test.js +++ b/packages/api/test/jsonl-tail-reader.test.js @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, test } from 'node:test'; -const { readJsonlTail } = await import('../dist/utils/jsonl-tail-reader.js'); +const { readJsonlTail } = await import('../dist/utils/parsing/jsonl-tail-reader.js'); function makeJsonlFile(entries) { const dir = mkdtempSync(join(tmpdir(), 'jsonl-tail-')); diff --git a/packages/api/test/kimi-agent-service.test.js b/packages/api/test/kimi-agent-service.test.js index 4246c096aa..b5fa598d13 100644 --- a/packages/api/test/kimi-agent-service.test.js +++ b/packages/api/test/kimi-agent-service.test.js @@ -14,7 +14,7 @@ writeFileSync(join(stubBinDir, 'kimi-cli'), '#!/bin/sh\nexit 1\n', { mode: 0o755 process.env.PATH = `${stubBinDir}:${process.env.PATH}`; const { KimiAgentService } = await import('../dist/domains/cats/services/agents/providers/KimiAgentService.js'); -const { invalidateCliCommand } = await import('../dist/utils/cli-resolve.js'); +const { invalidateCliCommand } = await import('../dist/utils/cli/cli-resolve.js'); async function collect(iterable) { const items = []; diff --git a/packages/api/test/local-override.test.js b/packages/api/test/local-override.test.js index 5d8a7a2231..5f7449ea8a 100644 --- a/packages/api/test/local-override.test.js +++ b/packages/api/test/local-override.test.js @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, it } from 'node:test'; -const { resolveWithLocalOverlay } = await import('../dist/utils/local-override.js'); +const { resolveWithLocalOverlay } = await import('../dist/utils/paths/local-override.js'); const TMP = join(tmpdir(), `local-override-test-${Date.now()}`); diff --git a/packages/api/test/memory-root.test.js b/packages/api/test/memory-root.test.js index ee4dff91b8..423a54f0c9 100644 --- a/packages/api/test/memory-root.test.js +++ b/packages/api/test/memory-root.test.js @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, it } from 'node:test'; -const { resolveMemoryRepoPaths } = await import('../dist/utils/memory-root.js'); +const { resolveMemoryRepoPaths } = await import('../dist/utils/paths/memory-root.js'); describe('resolveMemoryRepoPaths', () => { const tmpDirs = []; diff --git a/packages/api/test/monorepo-root.test.js b/packages/api/test/monorepo-root.test.js index 10f036f4a6..7640c74f3a 100644 --- a/packages/api/test/monorepo-root.test.js +++ b/packages/api/test/monorepo-root.test.js @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, it } from 'node:test'; -const { _clearCachesForTest, findMonorepoRoot } = await import('../dist/utils/monorepo-root.js'); +const { _clearCachesForTest, findMonorepoRoot } = await import('../dist/utils/paths/monorepo-root.js'); describe('findMonorepoRoot', () => { afterEach(() => { diff --git a/packages/api/test/mount-rules-route.test.js b/packages/api/test/mount-rules-route.test.js index 7192fb64bd..c70ad473e4 100644 --- a/packages/api/test/mount-rules-route.test.js +++ b/packages/api/test/mount-rules-route.test.js @@ -16,7 +16,7 @@ import { mountRulesRoutes } from '../dist/routes/mount-rules.js'; import { mountSkillSymlinks } from '../dist/skills/skill-manage.js'; import { syncAll } from '../dist/skills/skill-sync-all.js'; import { syncProject } from '../dist/skills/skill-sync-engine.js'; -import { resolveCatCafeSkillsSource } from '../dist/utils/skill-source.js'; +import { resolveCatCafeSkillsSource } from '../dist/utils/skills/skill-source.js'; function resolveRepoRoot() { return execFileSync('git', ['rev-parse', '--show-toplevel'], { diff --git a/packages/api/test/ndjson-parser.test.js b/packages/api/test/ndjson-parser.test.js index 505f851361..2bd466b4d1 100644 --- a/packages/api/test/ndjson-parser.test.js +++ b/packages/api/test/ndjson-parser.test.js @@ -7,7 +7,7 @@ import assert from 'node:assert/strict'; import { PassThrough } from 'node:stream'; import { test } from 'node:test'; -const { parseNDJSON, isParseError } = await import('../dist/utils/ndjson-parser.js'); +const { parseNDJSON, isParseError } = await import('../dist/utils/parsing/ndjson-parser.js'); /** Helper: collect all items from async iterable */ async function collect(iterable) { diff --git a/packages/api/test/normalize-error.test.js b/packages/api/test/normalize-error.test.js index 5853fa55f5..457d2cb86a 100644 --- a/packages/api/test/normalize-error.test.js +++ b/packages/api/test/normalize-error.test.js @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -const { normalizeErrorMessage } = await import('../dist/utils/normalize-error.js'); +const { normalizeErrorMessage } = await import('../dist/utils/parsing/normalize-error.js'); test('Error instance → .message', () => { assert.equal(normalizeErrorMessage(new Error('boom')), 'boom'); diff --git a/packages/api/test/orphan-chrome-cleaner.test.js b/packages/api/test/orphan-chrome-cleaner.test.js index a9d3c7218e..0846e6c5fe 100644 --- a/packages/api/test/orphan-chrome-cleaner.test.js +++ b/packages/api/test/orphan-chrome-cleaner.test.js @@ -3,7 +3,7 @@ import { describe, test } from 'node:test'; await import('tsx/esm'); const { cleanOrphanAgentBrowserChrome, parseAgentBrowserChromeCleanupPids, parseOrphanPids } = await import( - '../src/utils/orphan-chrome-cleaner.ts' + '../src/utils/process/orphan-chrome-cleaner.ts' ); const fakeLog = { diff --git a/packages/api/test/process-liveness-probe.test.js b/packages/api/test/process-liveness-probe.test.js index 132d07dbe2..3921c87931 100644 --- a/packages/api/test/process-liveness-probe.test.js +++ b/packages/api/test/process-liveness-probe.test.js @@ -5,7 +5,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -const { ProcessLivenessProbe } = await import('../dist/utils/ProcessLivenessProbe.js'); +const { ProcessLivenessProbe } = await import('../dist/utils/process/ProcessLivenessProbe.js'); async function waitForBusySilent(probe, { timeoutMs = 3_000, burnMs = 180, settleMs = 40 } = {}) { const deadline = Date.now() + timeoutMs; @@ -196,7 +196,7 @@ test( }, ); -const { parseCpuTime } = await import('../dist/utils/ProcessLivenessProbe.js'); +const { parseCpuTime } = await import('../dist/utils/process/ProcessLivenessProbe.js'); test('parseCpuTime handles mm:ss.SS format', () => { assert.equal(parseCpuTime('1:30.50'), (1 * 60 + 30.5) * 1000); diff --git a/packages/api/test/project-path.test.js b/packages/api/test/project-path.test.js index cc0a4e9b79..e07fb25932 100644 --- a/packages/api/test/project-path.test.js +++ b/packages/api/test/project-path.test.js @@ -12,7 +12,7 @@ const { getDefaultDeniedRoots, isPathUnderRoots, isDenylistMode, -} = await import('../dist/utils/project-path.js'); +} = await import('../dist/utils/paths/project-path.js'); describe('denylist mode (default)', () => { let savedAllowedRoots; diff --git a/packages/api/test/public-test-exclusions.test.js b/packages/api/test/public-test-exclusions.test.js index 6befaac796..3c4ce6ea8f 100644 --- a/packages/api/test/public-test-exclusions.test.js +++ b/packages/api/test/public-test-exclusions.test.js @@ -99,7 +99,7 @@ test('registry preserves metadata for active legacy exclusions and drops stale o category: 'source_only', owner: '@zts212653', introducedBy: '069d0f0fb', - expiresOn: '2026-06-30', + expiresOn: '2026-07-31', }, ); }); @@ -133,7 +133,7 @@ test('validator rejects malformed, expired, or zero-match exclusion entries', as category: 'source_only', reason: 'missing owner should fail', introducedBy: 'deadbeef0', - expiresOn: '2026-06-30', + expiresOn: '2026-07-31', }, ], }, @@ -177,7 +177,7 @@ test('validator rejects malformed, expired, or zero-match exclusion entries', as reason: 'stale entry should fail', owner: '@zts212653', introducedBy: 'deadbeef2', - expiresOn: '2026-06-30', + expiresOn: '2026-07-31', }, ], }, diff --git a/packages/api/test/queue-processor.test.js b/packages/api/test/queue-processor.test.js index c67dab3ae5..a07fdb8bbb 100644 --- a/packages/api/test/queue-processor.test.js +++ b/packages/api/test/queue-processor.test.js @@ -1595,6 +1595,7 @@ describe('QueueProcessor', () => { const entry = enqueueEntry(deps.queue, { source: 'connector', eventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:owner/repo#1'], }); deps.queue.backfillMessageId('t1', 'u1', entry.id, 'msg-connector-covered'); @@ -1606,6 +1607,7 @@ describe('QueueProcessor', () => { const opts = call.arguments[6]; assert.ok(opts && typeof opts === 'object', 'expected opts object'); assert.equal(opts.eventDrivenExternalWaitCoverage, true); + assert.deepEqual(opts.eventDrivenExternalWaitCoverageKeys, ['pr:owner/repo#1']); }); it('degrades when messageStore.getById throws: still executes without contentBlocks', async () => { diff --git a/packages/api/test/route-serial-routing-guard-remedial.test.js b/packages/api/test/route-serial-routing-guard-remedial.test.js index b3b3cb9d85..2fddc7eb4f 100644 --- a/packages/api/test/route-serial-routing-guard-remedial.test.js +++ b/packages/api/test/route-serial-routing-guard-remedial.test.js @@ -334,7 +334,7 @@ describe('F177 Phase H — route-serial routing guard remedial invoke', () => { codexService, 'thread-routing-guard-event-driven-coverage-per-cat', { opus: opusService }, - { routeOptions: { eventDrivenExternalWaitCoverage: true } }, + { routeOptions: { eventDrivenExternalWaitCoverage: true, eventDrivenExternalWaitCoverageKeys: ['pr:35'] } }, ); assert.equal(opusService.calls.length, 2, 'A2A target must not inherit connector callback coverage'); @@ -409,7 +409,7 @@ describe('F177 Phase H — route-serial routing guard remedial invoke', () => { 'thread-routing-guard-event-driven-remedial', {}, { - routeOptions: { eventDrivenExternalWaitCoverage: true }, + routeOptions: { eventDrivenExternalWaitCoverage: true, eventDrivenExternalWaitCoverageKeys: ['pr:35'] }, }, ); @@ -453,7 +453,7 @@ describe('F177 Phase H — route-serial routing guard remedial invoke', () => { service, 'thread-routing-guard-event-driven-remedial-signed', {}, - { routeOptions: { eventDrivenExternalWaitCoverage: true } }, + { routeOptions: { eventDrivenExternalWaitCoverage: true, eventDrivenExternalWaitCoverageKeys: ['pr:35'] } }, ); assert.equal(calls.length, 2, 'first-pass no-exit text should trigger one remedial invoke'); @@ -787,7 +787,10 @@ describe('F177 Phase H — route-serial routing guard remedial invoke', () => { 'thread-routing-guard-event-driven-wait', {}, { - routeOptions: { eventDrivenExternalWaitCoverage: true }, + routeOptions: { + eventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:clowder-labs/clowder-ai#32'], + }, }, ); @@ -869,6 +872,38 @@ describe('F177 Phase H — route-serial routing guard remedial invoke', () => { assert.match(codexMessages[0].content, /External Wait: event-driven/); }); + test('2b event-driven external wait rejects same-turn issue tracking coverage for PR ids', async () => { + const service = createSequenceService('codex', [ + [ + { + type: 'tool_use', + toolName: 'cat_cafe_register_issue_tracking', + toolInput: { repoFullName: 'clowder-labs/clowder-ai', issueNumber: 35 }, + }, + { + type: 'tool_result', + toolName: 'cat_cafe_register_issue_tracking', + content: '{"status":"ok","threadId":"thread-routing-guard-event-driven-issue-mismatch"}', + }, + { + type: 'text', + content: + '已注册 issue tracking,后续评论会结构化回调。\n\nExternal Wait: event-driven (pr:clowder-labs/clowder-ai#35)', + }, + ], + '@co-creator', + ]); + + const { appended, calls } = await runRoute(service, 'thread-routing-guard-event-driven-issue-mismatch'); + + assert.equal(calls.length, 2, 'issue tracking coverage must not validate a PR event-driven wait id'); + assert.equal( + appended.find((m) => m.source?.connector === 'routing-guard-failure'), + undefined, + 'valid follow-up remedial exit should avoid failure after rejecting the mismatched event wait', + ); + }); + test('2b event-driven external wait final slot without verified callback coverage still gets remedial invoke', async () => { const service = createSequenceService('codex', [ 'cloud / CI 也许会回调,不需要 hold_ball。\n\nExternal Wait: event-driven (pr:clowder-labs/clowder-ai#32)', diff --git a/packages/api/test/routing-guard-remedial.test.js b/packages/api/test/routing-guard-remedial.test.js index 2960e518ef..9daa574d19 100644 --- a/packages/api/test/routing-guard-remedial.test.js +++ b/packages/api/test/routing-guard-remedial.test.js @@ -82,6 +82,7 @@ describe('F177 Phase H — shouldRemediateRouting', () => { ...base, text: 'cloud / CI 已有结构化回调覆盖。\n\nExternal Wait: event-driven (pr:clowder-ai#32)', hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:clowder-ai#32'], needsGuard: true, attempted: false, }), @@ -89,6 +90,20 @@ describe('F177 Phase H — shouldRemediateRouting', () => { ); }); + test('2b External Wait id must match verified callback coverage', () => { + assert.equal( + shouldRemediateRouting({ + ...base, + text: 'issue comment 已有结构化回调覆盖。\n\nExternal Wait: event-driven (pr:clowder-ai#32)', + hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['issue:clowder-ai#32'], + needsGuard: true, + attempted: false, + }), + true, + ); + }); + test('2b External Wait event-driven 槽位 without verified callback coverage → still triggers remedial', () => { assert.equal( shouldRemediateRouting({ @@ -120,11 +135,24 @@ describe('F177 Phase H — hasValidRoutingExit', () => { ...base, text: '结论:已有结构化回调 + EYES>0,不续 hold_ball。\n\nExternal Wait: event-driven (github-pr-32)', hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['github-pr-32'], }), true, ); }); + test('External Wait: event-driven() rejects mismatched coverage id', () => { + assert.equal( + hasValidRoutingExit({ + ...base, + text: '结论:只有 issue comment 回调。\n\nExternal Wait: event-driven (pr:owner/repo#42)', + hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['issue:owner/repo#42'], + }), + false, + ); + }); + test('External Wait: event-driven() alone is not a valid routing exit', () => { assert.equal( hasValidRoutingExit({ diff --git a/packages/api/test/sanitize-cli-stderr.test.js b/packages/api/test/sanitize-cli-stderr.test.js index 7c96d85064..612fd0d9f1 100644 --- a/packages/api/test/sanitize-cli-stderr.test.js +++ b/packages/api/test/sanitize-cli-stderr.test.js @@ -5,7 +5,7 @@ import assert from 'node:assert'; import test from 'node:test'; -import { sanitizeCliStderr } from '../dist/utils/sanitize-cli-stderr.js'; +import { sanitizeCliStderr } from '../dist/utils/cli/sanitize-cli-stderr.js'; test('strips ANSI escape sequences', () => { const input = '\x1b[31mError\x1b[0m: thing'; diff --git a/packages/api/test/scheduler/cicd-check-spec.test.js b/packages/api/test/scheduler/cicd-check-spec.test.js index b47a9683bb..e94ffb5a22 100644 --- a/packages/api/test/scheduler/cicd-check-spec.test.js +++ b/packages/api/test/scheduler/cicd-check-spec.test.js @@ -133,6 +133,7 @@ describe('CiCdCheckTaskSpec', () => { assert.equal(policy.reason, 'github_ci_pass'); assert.equal(policy.suggestedSkill, 'merge-gate'); assert.equal(policy.eventDrivenExternalWaitCoverage, true); + assert.deepEqual(policy.eventDrivenExternalWaitCoverageKeys, ['pr:a/b#1']); }); it('execute triggers CI fail for default review intent without event-driven wait coverage', async () => { @@ -207,6 +208,7 @@ describe('CiCdCheckTaskSpec', () => { assert.equal(policy.priority, 'urgent'); assert.equal(policy.reason, 'github_ci_failure'); assert.equal(policy.eventDrivenExternalWaitCoverage, true); + assert.deepEqual(policy.eventDrivenExternalWaitCoverageKeys, ['pr:a/b#1']); }); it('gate filters out ci.enabled=false', async () => { diff --git a/packages/api/test/scheduler/review-feedback-spec.test.js b/packages/api/test/scheduler/review-feedback-spec.test.js index cc6354a95e..4d4b6eecf1 100644 --- a/packages/api/test/scheduler/review-feedback-spec.test.js +++ b/packages/api/test/scheduler/review-feedback-spec.test.js @@ -699,6 +699,11 @@ describe('ReviewFeedbackTaskSpec', () => { const policy = triggered[0][6]; assert.equal(policy.priority, 'urgent'); assert.equal(policy.suggestedSkill, 'receive-review'); + assert.notEqual( + policy.eventDrivenExternalWaitCoverage, + true, + 'CHANGES_REQUESTED wake must not claim future event-driven callback coverage before a re-trigger exists', + ); }); it('APPROVED triggers with suggestedSkill=merge-gate (Phase C)', async () => { @@ -777,6 +782,7 @@ describe('ReviewFeedbackTaskSpec', () => { assert.equal(policy.priority, 'normal'); assert.equal(policy.suggestedSkill, 'merge-gate'); assert.equal(policy.eventDrivenExternalWaitCoverage, true); + assert.deepEqual(policy.eventDrivenExternalWaitCoverageKeys, ['pr:owner/repo#42']); }); it('COMMENTED-only triggers with no suggestedSkill (Phase C)', async () => { @@ -808,6 +814,11 @@ describe('ReviewFeedbackTaskSpec', () => { const policy = triggered[0][6]; assert.equal(policy.priority, 'normal'); assert.equal(policy.suggestedSkill, undefined); + assert.notEqual( + policy.eventDrivenExternalWaitCoverage, + true, + 'COMMENTED wake must not claim future event-driven callback coverage before a re-trigger exists', + ); }); // ── #406: restart cursor persistence ── diff --git a/packages/api/test/skill-mount.test.js b/packages/api/test/skill-mount.test.js index eef0fa1851..bc35a4ffa6 100644 --- a/packages/api/test/skill-mount.test.js +++ b/packages/api/test/skill-mount.test.js @@ -9,7 +9,7 @@ const REPO_ROOT = resolve(API_DIR, '../..'); describe('resolveMainRepoPath', () => { it('falls back to the repository root when git is unavailable', () => { const script = ` -const mod = await import('./dist/utils/skill-mount.js'); +const mod = await import('./dist/utils/skills/skill-mount.js'); console.log(await mod.resolveMainRepoPath()); `; const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { diff --git a/packages/api/test/skills/drift-detector.test.js b/packages/api/test/skills/drift-detector.test.js index 93904b0f9b..19f18999c1 100644 --- a/packages/api/test/skills/drift-detector.test.js +++ b/packages/api/test/skills/drift-detector.test.js @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, test } from 'node:test'; import { DEFAULT_MOUNT_RULES } from '@cat-cafe/shared'; import { checkGlobal, checkProject } from '../../dist/skills/drift-detector.js'; -import { listSourceSkillNames } from '../../dist/utils/skill-source.js'; +import { listSourceSkillNames } from '../../dist/utils/skills/skill-source.js'; /** * Test helper: wraps checkGlobal for mount-level drift testing. diff --git a/packages/api/test/skills/drift-resolver.test.js b/packages/api/test/skills/drift-resolver.test.js index 9e10ad5923..c715a46f3d 100644 --- a/packages/api/test/skills/drift-resolver.test.js +++ b/packages/api/test/skills/drift-resolver.test.js @@ -13,7 +13,7 @@ import { import { checkGlobal } from '../../dist/skills/drift-detector.js'; import { syncDrift } from '../../dist/skills/drift-resolver.js'; import { syncProject } from '../../dist/skills/skill-sync-engine.js'; -import { checkStaleness, listSourceSkillNames } from '../../dist/utils/skill-source.js'; +import { checkStaleness, listSourceSkillNames } from '../../dist/utils/skills/skill-source.js'; /** * Test helper: wraps checkGlobal for mount-level drift detection. diff --git a/packages/api/test/tcp-probe.test.js b/packages/api/test/tcp-probe.test.js index d8a0d30730..5dc5683d12 100644 --- a/packages/api/test/tcp-probe.test.js +++ b/packages/api/test/tcp-probe.test.js @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createServer } from 'node:net'; import { describe, it } from 'node:test'; -const { tcpProbe } = await import('../dist/utils/tcp-probe.js'); +const { tcpProbe } = await import('../dist/utils/network/tcp-probe.js'); describe('tcpProbe', () => { it('returns true for a listening port', async () => { diff --git a/packages/api/test/telemetry/cli-spawn-redaction.test.js b/packages/api/test/telemetry/cli-spawn-redaction.test.js index 1059d49bac..cf26bada30 100644 --- a/packages/api/test/telemetry/cli-spawn-redaction.test.js +++ b/packages/api/test/telemetry/cli-spawn-redaction.test.js @@ -16,7 +16,7 @@ import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const CLI_SPAWN_SRC = resolve(__dirname, '../../src/utils/cli-spawn.ts'); +const CLI_SPAWN_SRC = resolve(__dirname, '../../src/utils/cli/cli-spawn.ts'); test('F152: cli-spawn Windows shim debug log must not contain args field', async (t) => { // Read the source file and find the Windows shim debug log line diff --git a/packages/api/test/telemetry/observability-coverage.test.js b/packages/api/test/telemetry/observability-coverage.test.js index e2922da9a0..4b618206c4 100644 --- a/packages/api/test/telemetry/observability-coverage.test.js +++ b/packages/api/test/telemetry/observability-coverage.test.js @@ -39,7 +39,7 @@ test('F153: liveness probe register/unregister lifecycle', async (t) => { }); test('F153: cli-spawn wires liveness probes', () => { - const source = readFileSync(resolve(__dirname, '../../src/utils/cli-spawn.ts'), 'utf8'); + const source = readFileSync(resolve(__dirname, '../../src/utils/cli/cli-spawn.ts'), 'utf8'); // Must import both register and unregister assert.ok(source.includes('registerLivenessProbe'), 'cli-spawn must import registerLivenessProbe'); diff --git a/packages/api/test/telemetry/otel-tracing-phase-b.test.js b/packages/api/test/telemetry/otel-tracing-phase-b.test.js index 33bf4b9b2c..4e8d6a0973 100644 --- a/packages/api/test/telemetry/otel-tracing-phase-b.test.js +++ b/packages/api/test/telemetry/otel-tracing-phase-b.test.js @@ -17,8 +17,8 @@ import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const CLI_SPAWN_SRC = resolve(__dirname, '../../src/utils/cli-spawn.ts'); -const CLI_TYPES_SRC = resolve(__dirname, '../../src/utils/cli-types.ts'); +const CLI_SPAWN_SRC = resolve(__dirname, '../../src/utils/cli/cli-spawn.ts'); +const CLI_TYPES_SRC = resolve(__dirname, '../../src/utils/cli/cli-types.ts'); const TYPES_SRC = resolve(__dirname, '../../src/domains/cats/services/types.ts'); const CLAUDE_SERVICE_SRC = resolve(__dirname, '../../src/domains/cats/services/agents/providers/ClaudeAgentService.ts'); const INVOKE_SRC = resolve(__dirname, '../../src/domains/cats/services/agents/invocation/invoke-single-cat.ts'); diff --git a/packages/api/test/telemetry/otel-tracing-runtime.test.js b/packages/api/test/telemetry/otel-tracing-runtime.test.js index dda1bad26a..a485b2f034 100644 --- a/packages/api/test/telemetry/otel-tracing-runtime.test.js +++ b/packages/api/test/telemetry/otel-tracing-runtime.test.js @@ -20,7 +20,7 @@ const { InMemorySpanExporter, SimpleSpanProcessor } = await import('@opentelemet const { NodeTracerProvider } = await import('@opentelemetry/sdk-trace-node'); // Module under test -const { spawnCli } = await import('../../dist/utils/cli-spawn.js'); +const { spawnCli } = await import('../../dist/utils/cli/cli-spawn.js'); /** Collect all items from an async iterable */ async function collect(iterable) { diff --git a/packages/api/test/utils/is-same-repo.test.js b/packages/api/test/utils/is-same-repo.test.js index 5483a7eb98..187a2ffc9f 100644 --- a/packages/api/test/utils/is-same-repo.test.js +++ b/packages/api/test/utils/is-same-repo.test.js @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { after, before, describe, it } from 'node:test'; -import { initRepoIdentity, isSameRepo } from '../../dist/utils/is-same-repo.js'; +import { initRepoIdentity, isSameRepo } from '../../dist/utils/paths/is-same-repo.js'; function configureTestRepo(repoPath) { execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: repoPath }); diff --git a/packages/api/test/utils/skill-mount-targets.test.js b/packages/api/test/utils/skill-mount-targets.test.js index 4c16ae4b03..e621f8be43 100644 --- a/packages/api/test/utils/skill-mount-targets.test.js +++ b/packages/api/test/utils/skill-mount-targets.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import { DEFAULT_MOUNT_RULES } from '@cat-cafe/shared'; -import { buildMountPointDirCandidates, buildSkillMountTargets } from '../../dist/utils/skill-mount.js'; +import { buildMountPointDirCandidates, buildSkillMountTargets } from '../../dist/utils/skills/skill-mount.js'; const PROJECT = '/tmp/proj'; const HOME = '/Users/test'; diff --git a/packages/api/test/utils/skill-source.test.js b/packages/api/test/utils/skill-source.test.js index 2a90daf289..ddc271edb5 100644 --- a/packages/api/test/utils/skill-source.test.js +++ b/packages/api/test/utils/skill-source.test.js @@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'node:path'; import { describe, test } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { resolveCatCafeSkillsSource } from '../../dist/utils/skill-source.js'; +import { resolveCatCafeSkillsSource } from '../../dist/utils/skills/skill-source.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, '../../../..'); @@ -15,7 +15,7 @@ describe('Cat Cafe skills source resolver', () => { }); test('does not route lifecycle skill source through resolveMainRepoPath', async () => { - const resolverSource = await readFile(join(repoRoot, 'packages/api/src/utils/skill-source.ts'), 'utf-8'); + const resolverSource = await readFile(join(repoRoot, 'packages/api/src/utils/skills/skill-source.ts'), 'utf-8'); assert.doesNotMatch(resolverSource, /resolveMainRepoPath/); }); diff --git a/packages/api/test/verdict-detect.test.js b/packages/api/test/verdict-detect.test.js index 73cfb7b6b1..bbef9e581f 100644 --- a/packages/api/test/verdict-detect.test.js +++ b/packages/api/test/verdict-detect.test.js @@ -294,6 +294,7 @@ describe('F167 C2 AC-C7: shouldWarnVerdictWithoutPass', () => { toolNames: [], structuredTargetCats: [], hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:35'], }), false, ); diff --git a/packages/api/test/void-hold-detect.test.js b/packages/api/test/void-hold-detect.test.js index 8217f3f67c..2eaa40fdc3 100644 --- a/packages/api/test/void-hold-detect.test.js +++ b/packages/api/test/void-hold-detect.test.js @@ -137,6 +137,7 @@ describe('F167 Phase I AC-I1: shouldWarnVoidHold', () => { ...base, text: '不需要 hold_ball;这是 2b 事件驱动等待。\nExternal Wait: event-driven (pr:35)', hasEventDrivenExternalWaitCoverage: true, + eventDrivenExternalWaitCoverageKeys: ['pr:35'], }); assert.equal(result.shouldEmit, false); assert.equal(result.matchedPattern, 'en_hold_ball_underscore'); diff --git a/packages/web/src/components/HubCatEditor.tsx b/packages/web/src/components/HubCatEditor.tsx index 220bb9a0fd..3da1519725 100644 --- a/packages/web/src/components/HubCatEditor.tsx +++ b/packages/web/src/components/HubCatEditor.tsx @@ -343,12 +343,8 @@ export function HubCatEditor({ cat, draft, existingCats, open, onClose, onSaved ? { clientId: t.runtimeDefaults.clientId, defaultModel: t.runtimeDefaults.defaultModel, - ...(t.runtimeDefaults.catAgentProtocol - ? { catAgentProtocol: t.runtimeDefaults.catAgentProtocol } - : {}), - ...(t.runtimeDefaults.nativeToolLevel - ? { nativeToolLevel: t.runtimeDefaults.nativeToolLevel } - : {}), + ...(t.runtimeDefaults.catAgentProtocol ? { catAgentProtocol: t.runtimeDefaults.catAgentProtocol } : {}), + ...(t.runtimeDefaults.nativeToolLevel ? { nativeToolLevel: t.runtimeDefaults.nativeToolLevel } : {}), } : {}), }); diff --git a/packages/web/src/components/__tests__/first-run-quest-wizard.test.tsx b/packages/web/src/components/__tests__/first-run-quest-wizard.test.tsx index 14925164d5..86dd8aff22 100644 --- a/packages/web/src/components/__tests__/first-run-quest-wizard.test.tsx +++ b/packages/web/src/components/__tests__/first-run-quest-wizard.test.tsx @@ -336,9 +336,7 @@ describe('FirstRunQuestWizard', () => { await flushEffects(); // Step 1: select 幼仔 template - const templateButton = Array.from(document.querySelectorAll('button')).find((b) => - b.textContent?.includes('幼猫'), - ); + const templateButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.includes('幼猫')); expect(templateButton).toBeTruthy(); await act(async () => { templateButton!.click(); diff --git a/packages/web/src/components/first-run-quest/TemplateStep.tsx b/packages/web/src/components/first-run-quest/TemplateStep.tsx index ed07f375d0..aa3b9928d5 100644 --- a/packages/web/src/components/first-run-quest/TemplateStep.tsx +++ b/packages/web/src/components/first-run-quest/TemplateStep.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useEffect, useState } from 'react'; import type { CatAgentProtocol, NativeToolLevel } from '@cat-cafe/shared'; +import { useEffect, useState } from 'react'; import { apiFetch } from '@/utils/api-client'; import type { ClientId } from '../hub-cat-editor.model';