diff --git a/.github/workflows/sync-upstream-main.yml b/.github/workflows/sync-upstream-main.yml index 50941a08a1..9bf0ce4f83 100644 --- a/.github/workflows/sync-upstream-main.yml +++ b/.github/workflows/sync-upstream-main.yml @@ -56,13 +56,26 @@ jobs: - name: Merge upstream into main id: main + # Auto-conflict resolution: when upstream/main conflicts with fork/main, + # upstream wins (-X theirs). fork/main's job is to track upstream; any + # fork-specific code belongs on develop. Verdict/eval commits on main + # add new files and rarely conflict with upstream code changes. + # The gate step below still validates the merged result before pushing. run: | set -euo pipefail git switch -C main origin/main git reset --hard origin/main before="$(git rev-parse HEAD)" - git merge --no-edit upstream/main + + if git merge --no-edit upstream/main; then + echo "Clean merge (no conflicts)" + else + echo "::warning::upstream→main merge conflict detected — auto-resolving (upstream wins: -X theirs)" + git merge --abort + git merge -X theirs --no-edit upstream/main + fi + after="$(git rev-parse HEAD)" if [ "$before" = "$after" ]; then @@ -123,6 +136,11 @@ jobs: - name: Prepare develop sync branch id: develop + # Auto-conflict resolution: when main conflicts with develop, develop + # wins (-X ours). develop is the active fork branch with feature work; + # upstream changes that conflict with fork code should not silently + # overwrite them. Non-conflicting upstream changes still merge in + # normally. The PR gives reviewers a chance to inspect the result. run: | set -euo pipefail git switch -C "$SYNC_BRANCH" origin/develop @@ -133,7 +151,13 @@ jobs: exit 0 fi - git merge --no-edit main + if git merge --no-edit main; then + echo "Clean merge (no conflicts)" + else + echo "::warning::main→develop merge conflict detected — auto-resolving (develop wins: -X ours)" + git merge --abort + git merge -X ours --no-edit main + fi if git diff --quiet origin/develop HEAD; then echo "needs_pr=false" >> "$GITHUB_OUTPUT" diff --git "a/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/b1-session-bootstrap.md" "b/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/b1-session-bootstrap.md" new file mode 100644 index 0000000000..8d04a9aa98 --- /dev/null +++ "b/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/b1-session-bootstrap.md" @@ -0,0 +1,4 @@ + + + +[Session Bootstrap: initialized] diff --git "a/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/hook.yaml" "b/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/hook.yaml" new file mode 100644 index 0000000000..060d3d2a84 --- /dev/null +++ "b/assets/prompt-hooks/b1-\344\274\232\350\257\235\345\274\225\345\257\274/hook.yaml" @@ -0,0 +1,26 @@ +id: B1 +name: 会话引导 +stage: session-init +order: 2100 +version: 1 +enabled: true + +# Content resolution +template: b1-session-bootstrap.md +resolver: B1BootstrapResolver + +# Dependencies +inputs: + - isNewSession + - bootstrapData + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "新会话的初始化引导信息" diff --git "a/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/.template-ref" "b/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/.template-ref" new file mode 100644 index 0000000000..075604e7dc --- /dev/null +++ "b/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/c1-mcp-callback.md diff --git "a/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/hook.yaml" "b/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/hook.yaml" new file mode 100644 index 0000000000..970ac9e81c --- /dev/null +++ "b/assets/prompt-hooks/c1-mcp-\345\233\236\350\260\203/hook.yaml" @@ -0,0 +1,26 @@ +id: C1 +name: MCP 回调 +stage: session-init +order: 2200 +version: 1 +enabled: true + +# Content resolution +template: c1-mcp-callback.md +resolver: C1McpCallbackResolver + +# Dependencies +inputs: + - mcpAvailable + - mcpCallbackContent + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: editable +transparencyTier: opt-in-view +governanceTier: auto-evolve + +# CVO-facing +userExplanation: "MCP 服务可用时注入的回调提示" diff --git "a/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/.template-ref" "b/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/.template-ref" new file mode 100644 index 0000000000..89cae7ec7a --- /dev/null +++ "b/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d1-identity-anchor.md diff --git "a/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/hook.yaml" "b/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/hook.yaml" new file mode 100644 index 0000000000..cff7e4fa90 --- /dev/null +++ "b/assets/prompt-hooks/d1-\350\272\253\344\273\275\351\224\232\345\256\232/hook.yaml" @@ -0,0 +1,25 @@ +id: D1 +name: 身份锚定 +stage: per-turn +order: 100 +version: 1 +enabled: true + +# Content resolution +template: d1-identity-anchor.md +resolver: D1IdentityAnchorResolver + +# Dependencies +inputs: + - identity + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "每轮重申身份以防偏移" diff --git "a/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/.template-ref" "b/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/.template-ref" new file mode 100644 index 0000000000..e724eda570 --- /dev/null +++ "b/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d10-critique-tag.md diff --git "a/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/hook.yaml" "b/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/hook.yaml" new file mode 100644 index 0000000000..e95c84e1d2 --- /dev/null +++ "b/assets/prompt-hooks/d10-\346\211\271\350\257\204\346\240\207\347\255\276/hook.yaml" @@ -0,0 +1,25 @@ +id: D10 +name: 批评标签 +stage: per-turn +order: 1000 +version: 1 +enabled: true + +# Content resolution +template: d10-critique-tag.md +resolver: D10CritiqueTagResolver + +# Dependencies +inputs: + - critiqueTag + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "用于标记需要批判性审视的内容" diff --git "a/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/.template-ref" "b/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/.template-ref" new file mode 100644 index 0000000000..a3ded334f1 --- /dev/null +++ "b/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d11-skill-trigger.md diff --git "a/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/hook.yaml" "b/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/hook.yaml" new file mode 100644 index 0000000000..f27321a4a8 --- /dev/null +++ "b/assets/prompt-hooks/d11-skill-\350\247\246\345\217\221/hook.yaml" @@ -0,0 +1,25 @@ +id: D11 +name: Skill 触发 +stage: per-turn +order: 1100 +version: 1 +enabled: true + +# Content resolution +template: d11-skill-trigger.md +resolver: D11SkillTriggerResolver + +# Dependencies +inputs: + - skillTrigger + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "自动触发的 skill 上下文" diff --git "a/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/d12-active-participants.md" "b/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/d12-active-participants.md" new file mode 100644 index 0000000000..db4e472485 --- /dev/null +++ "b/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/d12-active-participants.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/hook.yaml" "b/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/hook.yaml" new file mode 100644 index 0000000000..188309612a --- /dev/null +++ "b/assets/prompt-hooks/d12-\346\264\273\350\267\203\345\217\202\344\270\216\350\200\205/hook.yaml" @@ -0,0 +1,25 @@ +id: D12 +name: 活跃参与者 +stage: per-turn +order: 1200 +version: 1 +enabled: true + +# Content resolution +template: d12-active-participants.md +resolver: D12ActiveParticipantsResolver + +# Dependencies +inputs: + - activeParticipants + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前 thread 中活跃的猫猫列表" diff --git "a/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/.template-ref" "b/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/.template-ref" new file mode 100644 index 0000000000..a36b0b5dc0 --- /dev/null +++ "b/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d13-routing-policy.md diff --git "a/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/hook.yaml" "b/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/hook.yaml" new file mode 100644 index 0000000000..5f5cf7c722 --- /dev/null +++ "b/assets/prompt-hooks/d13-\350\267\257\347\224\261\347\255\226\347\225\245/hook.yaml" @@ -0,0 +1,25 @@ +id: D13 +name: 路由策略 +stage: per-turn +order: 1300 +version: 1 +enabled: true + +# Content resolution +template: d13-routing-policy.md +resolver: D13RoutingPolicyResolver + +# Dependencies +inputs: + - routingPolicy + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前的消息路由策略" diff --git "a/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/d14-sop-stage-hint.md" "b/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/d14-sop-stage-hint.md" new file mode 100644 index 0000000000..904b873f0a --- /dev/null +++ "b/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/d14-sop-stage-hint.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/hook.yaml" "b/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/hook.yaml" new file mode 100644 index 0000000000..d04676b1fe --- /dev/null +++ "b/assets/prompt-hooks/d14-sop-\351\230\266\346\256\265\346\217\220\347\244\272/hook.yaml" @@ -0,0 +1,25 @@ +id: D14 +name: SOP 阶段提示 +stage: per-turn +order: 1400 +version: 1 +enabled: true + +# Content resolution +template: d14-sop-stage-hint.md +resolver: D14SopStageResolver + +# Dependencies +inputs: + - sopStageHint + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前所处的 SOP 工作阶段" diff --git "a/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/.template-ref" "b/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/.template-ref" new file mode 100644 index 0000000000..f76b32cf1e --- /dev/null +++ "b/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d15-voice-on.md diff --git "a/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/hook.yaml" "b/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/hook.yaml" new file mode 100644 index 0000000000..45bf694de3 --- /dev/null +++ "b/assets/prompt-hooks/d15-\350\257\255\351\237\263\346\250\241\345\274\217/hook.yaml" @@ -0,0 +1,25 @@ +id: D15 +name: 语音模式 +stage: per-turn +order: 1500 +version: 1 +enabled: true + +# Content resolution +template: d15-voice-on.md +resolver: D15VoiceModeResolver + +# Dependencies +inputs: + - voiceMode + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: human-gated + +# CVO-facing +userExplanation: "语音模式开关状态和行为指导" diff --git "a/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/d16-bootcamp-mode.md" "b/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/d16-bootcamp-mode.md" new file mode 100644 index 0000000000..86dde438d0 --- /dev/null +++ "b/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/d16-bootcamp-mode.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/hook.yaml" "b/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/hook.yaml" new file mode 100644 index 0000000000..fcbea95e07 --- /dev/null +++ "b/assets/prompt-hooks/d16-bootcamp-\346\250\241\345\274\217/hook.yaml" @@ -0,0 +1,25 @@ +id: D16 +name: Bootcamp 模式 +stage: per-turn +order: 1600 +version: 1 +enabled: true + +# Content resolution +template: d16-bootcamp-mode.md +resolver: D16BootcampResolver + +# Dependencies +inputs: + - bootcampMode + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "新用户引导模式的上下文" diff --git "a/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/.template-ref" "b/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/.template-ref" new file mode 100644 index 0000000000..4d4f670224 --- /dev/null +++ "b/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d17-guide-candidate.md diff --git "a/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/hook.yaml" "b/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/hook.yaml" new file mode 100644 index 0000000000..bffa105fb1 --- /dev/null +++ "b/assets/prompt-hooks/d17-\345\274\225\345\257\274\345\200\231\351\200\211/hook.yaml" @@ -0,0 +1,25 @@ +id: D17 +name: 引导候选 +stage: per-turn +order: 1700 +version: 1 +enabled: true + +# Content resolution +template: d17-guide-candidate.md +resolver: D17GuideCandidateResolver + +# Dependencies +inputs: + - guideCandidate + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前可触发的引导流程候选" diff --git "a/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/.template-ref" "b/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/.template-ref" new file mode 100644 index 0000000000..5ac7bb33ed --- /dev/null +++ "b/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d18-world-context.md diff --git "a/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/hook.yaml" "b/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/hook.yaml" new file mode 100644 index 0000000000..4595767f05 --- /dev/null +++ "b/assets/prompt-hooks/d18-\344\270\226\347\225\214\344\270\212\344\270\213\346\226\207/hook.yaml" @@ -0,0 +1,25 @@ +id: D18 +name: 世界上下文 +stage: per-turn +order: 1800 +version: 1 +enabled: true + +# Content resolution +template: d18-world-context.md +resolver: D18WorldContextResolver + +# Dependencies +inputs: + - worldContext + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前世界/项目的环境上下文" diff --git "a/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/.template-ref" "b/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/.template-ref" new file mode 100644 index 0000000000..f26c903f0b --- /dev/null +++ "b/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d19-constitutional-knowledge.md diff --git "a/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/hook.yaml" "b/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/hook.yaml" new file mode 100644 index 0000000000..6b1953f1f7 --- /dev/null +++ "b/assets/prompt-hooks/d19-\345\256\252\346\263\225\347\237\245\350\257\206/hook.yaml" @@ -0,0 +1,25 @@ +id: D19 +name: 宪法知识 +stage: per-turn +order: 1900 +version: 1 +enabled: true + +# Content resolution +template: d19-constitutional-knowledge.md +resolver: D19ConstitutionalResolver + +# Dependencies +inputs: + - constitutionalKnowledge + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "核心治理知识和决策框架" diff --git "a/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/d2-direct-message-source.md" "b/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/d2-direct-message-source.md" new file mode 100644 index 0000000000..f0d854f002 --- /dev/null +++ "b/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/d2-direct-message-source.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/hook.yaml" "b/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/hook.yaml" new file mode 100644 index 0000000000..15e6c4e38d --- /dev/null +++ "b/assets/prompt-hooks/d2-\347\233\264\346\216\245\346\266\210\346\201\257\346\235\245\346\272\220/hook.yaml" @@ -0,0 +1,25 @@ +id: D2 +name: 直接消息来源 +stage: per-turn +order: 200 +version: 1 +enabled: true + +# Content resolution +template: d2-direct-message-source.md +resolver: D2DirectMsgResolver + +# Dependencies +inputs: + - directMessageSource + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: human-gated + +# CVO-facing +userExplanation: "标记消息是来自哪只猫或铲屎官" diff --git "a/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/.template-ref" "b/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/.template-ref" new file mode 100644 index 0000000000..d9d802aa50 --- /dev/null +++ "b/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d20-signal-articles.md diff --git "a/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/hook.yaml" "b/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/hook.yaml" new file mode 100644 index 0000000000..3153f9927d --- /dev/null +++ "b/assets/prompt-hooks/d20-\344\277\241\345\217\267\346\226\207\347\253\240/hook.yaml" @@ -0,0 +1,25 @@ +id: D20 +name: 信号文章 +stage: per-turn +order: 2000 +version: 1 +enabled: true + +# Content resolution +template: d20-signal-articles.md +resolver: D20SignalArticlesResolver + +# Dependencies +inputs: + - signalArticles + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "相关的信号/知识文章摘要" diff --git "a/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/.template-ref" "b/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/.template-ref" new file mode 100644 index 0000000000..00de9ee3bf --- /dev/null +++ "b/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/handoff-decision-tree.md diff --git "a/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/hook.yaml" "b/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/hook.yaml" new file mode 100644 index 0000000000..25652b7988 --- /dev/null +++ "b/assets/prompt-hooks/d21-\345\206\263\347\255\226\346\240\221/hook.yaml" @@ -0,0 +1,25 @@ +id: D21 +name: 决策树 +stage: per-turn +order: 2100 +version: 1 +enabled: true + +# Content resolution +template: handoff-decision-tree.md +resolver: D21DecisionTreeResolver + +# Dependencies +inputs: + - handoffDecisionTree + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "传球决策的完整判断树" diff --git "a/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/.template-ref" "b/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/.template-ref" new file mode 100644 index 0000000000..19ec57862c --- /dev/null +++ "b/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d3-same-breed-warning.md diff --git "a/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/hook.yaml" "b/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/hook.yaml" new file mode 100644 index 0000000000..bffffcefba --- /dev/null +++ "b/assets/prompt-hooks/d3-\345\220\214\346\227\217\350\255\246\345\221\212/hook.yaml" @@ -0,0 +1,25 @@ +id: D3 +name: 同族警告 +stage: per-turn +order: 300 +version: 1 +enabled: true + +# Content resolution +template: d3-same-breed-warning.md +resolver: D3SameBreedResolver + +# Dependencies +inputs: + - sameBreedWarning + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当同族猫互相 review 时给出警告" diff --git "a/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/.template-ref" "b/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/.template-ref" new file mode 100644 index 0000000000..dcc96793ec --- /dev/null +++ "b/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d4-cross-thread-reply.md diff --git "a/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/hook.yaml" "b/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/hook.yaml" new file mode 100644 index 0000000000..9b736b0928 --- /dev/null +++ "b/assets/prompt-hooks/d4-\350\267\250-thread-\345\233\236\345\244\215/hook.yaml" @@ -0,0 +1,25 @@ +id: D4 +name: 跨 Thread 回复 +stage: per-turn +order: 400 +version: 1 +enabled: true + +# Content resolution +template: d4-cross-thread-reply.md +resolver: D4CrossThreadResolver + +# Dependencies +inputs: + - crossThreadReplyHint + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: human-gated + +# CVO-facing +userExplanation: "收到跨 thread 投递时的上下文提示" diff --git "a/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/.template-ref" "b/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/.template-ref" new file mode 100644 index 0000000000..bbdc4de039 --- /dev/null +++ "b/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d5-ping-pong-warning.md diff --git "a/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/hook.yaml" "b/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/hook.yaml" new file mode 100644 index 0000000000..cefcd830c6 --- /dev/null +++ "b/assets/prompt-hooks/d5-\344\271\222\344\271\223\347\220\203\350\255\246\345\221\212/hook.yaml" @@ -0,0 +1,25 @@ +id: D5 +name: 乒乓球警告 +stage: per-turn +order: 500 +version: 1 +enabled: true + +# Content resolution +template: d5-ping-pong-warning.md +resolver: D5PingPongResolver + +# Dependencies +inputs: + - pingPongWarning + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: limited-edit +transparencyTier: visible-by-default +governanceTier: human-gated + +# CVO-facing +userExplanation: "当两只猫连续互传 ≥2 轮时警告" diff --git "a/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/d6-teammates-context.md" "b/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/d6-teammates-context.md" new file mode 100644 index 0000000000..c68f7eeecf --- /dev/null +++ "b/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/d6-teammates-context.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/hook.yaml" "b/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/hook.yaml" new file mode 100644 index 0000000000..93dd93f537 --- /dev/null +++ "b/assets/prompt-hooks/d6-\351\230\237\345\217\213\344\270\212\344\270\213\346\226\207/hook.yaml" @@ -0,0 +1,25 @@ +id: D6 +name: 队友上下文 +stage: per-turn +order: 600 +version: 1 +enabled: true + +# Content resolution +template: d6-teammates-context.md +resolver: D6TeammatesContextResolver + +# Dependencies +inputs: + - teammatesContext + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前活跃队友的实时状态" diff --git "a/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/d7-mode-declaration.md" "b/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/d7-mode-declaration.md" new file mode 100644 index 0000000000..4737be92d5 --- /dev/null +++ "b/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/d7-mode-declaration.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/hook.yaml" "b/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/hook.yaml" new file mode 100644 index 0000000000..aef6658d0f --- /dev/null +++ "b/assets/prompt-hooks/d7-\346\250\241\345\274\217\345\243\260\346\230\216/hook.yaml" @@ -0,0 +1,25 @@ +id: D7 +name: 模式声明 +stage: per-turn +order: 700 +version: 1 +enabled: true + +# Content resolution +template: d7-mode-declaration.md +resolver: D7ModeResolver + +# Dependencies +inputs: + - mode + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前的工作模式(如 voice mode、bootcamp 等)" diff --git "a/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/.template-ref" "b/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/.template-ref" new file mode 100644 index 0000000000..793df7b319 --- /dev/null +++ "b/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/a2a-ball-check.md diff --git "a/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/hook.yaml" "b/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/hook.yaml" new file mode 100644 index 0000000000..36c7e17130 --- /dev/null +++ "b/assets/prompt-hooks/d8-a2a-\347\220\203\346\235\203\346\243\200\346\237\245/hook.yaml" @@ -0,0 +1,25 @@ +id: D8 +name: A2A 球权检查 +stage: per-turn +order: 800 +version: 1 +enabled: true + +# Content resolution +template: a2a-ball-check.md +resolver: D8BallCheckResolver + +# Dependencies +inputs: + - ballCustody + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "检查当前球权状态,防止无球操作" diff --git "a/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/.template-ref" "b/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/.template-ref" new file mode 100644 index 0000000000..b8e8a32c1d --- /dev/null +++ "b/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/d9-routing-feedback.md diff --git "a/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/hook.yaml" "b/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/hook.yaml" new file mode 100644 index 0000000000..d2e4426ebb --- /dev/null +++ "b/assets/prompt-hooks/d9-\350\267\257\347\224\261\345\217\215\351\246\210/hook.yaml" @@ -0,0 +1,25 @@ +id: D9 +name: 路由反馈 +stage: per-turn +order: 900 +version: 1 +enabled: true + +# Content resolution +template: d9-routing-feedback.md +resolver: D9RoutingFeedbackResolver + +# Dependencies +inputs: + - routingFeedback + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "上一轮路由决策的反馈" diff --git "a/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/.template-ref" "b/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/.template-ref" new file mode 100644 index 0000000000..ecf69312ad --- /dev/null +++ "b/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l1-parallel-world.md diff --git "a/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/hook.yaml" "b/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/hook.yaml" new file mode 100644 index 0000000000..2bebf580f2 --- /dev/null +++ "b/assets/prompt-hooks/l1-\345\271\263\350\241\214\344\270\226\347\225\214\350\207\252\346\210\221\346\204\217\350\257\206/hook.yaml" @@ -0,0 +1,24 @@ +id: L1 +name: 平行世界自我意识 +stage: session-init +order: 100 +version: 1 +enabled: true + +# Content resolution +template: l1-parallel-world.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "告诉猫同一个身份可能在多个对话中同时存在" diff --git "a/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/.template-ref" "b/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/.template-ref" new file mode 100644 index 0000000000..ebf5f84125 --- /dev/null +++ "b/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l2-carry-over.md diff --git "a/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/hook.yaml" "b/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/hook.yaml" new file mode 100644 index 0000000000..57bd1aac51 --- /dev/null +++ "b/assets/prompt-hooks/l2-\345\256\242\350\247\202\346\200\247-carry-over-\346\256\265/hook.yaml" @@ -0,0 +1,24 @@ +id: L2 +name: 客观性 carry-over 段 +stage: session-init +order: 200 +version: 1 +enabled: true + +# Content resolution +template: l2-carry-over.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "确保猫的基础能力不因自定义 prompt 而退化" diff --git "a/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/.template-ref" "b/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/.template-ref" new file mode 100644 index 0000000000..761bccc964 --- /dev/null +++ "b/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l3-routing-rules.md diff --git "a/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/hook.yaml" "b/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/hook.yaml" new file mode 100644 index 0000000000..e50f328f63 --- /dev/null +++ "b/assets/prompt-hooks/l3-\344\274\240\347\220\203\344\270\211\351\200\211\344\270\200-\350\267\257\347\224\261\350\247\204\345\210\231/hook.yaml" @@ -0,0 +1,24 @@ +id: L3 +name: 传球三选一 + @ 路由规则 +stage: session-init +order: 300 +version: 1 +enabled: true + +# Content resolution +template: l3-routing-rules.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "规定猫每轮对话结束时必须决定球传给谁" diff --git "a/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/.template-ref" "b/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/.template-ref" new file mode 100644 index 0000000000..c9b6f7ba34 --- /dev/null +++ "b/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l4-iron-laws.md diff --git "a/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/hook.yaml" "b/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/hook.yaml" new file mode 100644 index 0000000000..c1f84588b1 --- /dev/null +++ "b/assets/prompt-hooks/l4-\344\272\224\346\235\241\351\223\201\345\276\213/hook.yaml" @@ -0,0 +1,24 @@ +id: L4 +name: 五条铁律 +stage: session-init +order: 400 +version: 1 +enabled: true + +# Content resolution +template: l4-iron-laws.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "不可违反的安全约束" diff --git "a/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/hook.yaml" "b/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/hook.yaml" new file mode 100644 index 0000000000..ff427a01a5 --- /dev/null +++ "b/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/hook.yaml" @@ -0,0 +1,24 @@ +id: L5 +name: MCP 工具 quick index +stage: session-init +order: 500 +version: 1 +enabled: true + +# Content resolution +template: l5-mcp-tools-index.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "MCP 工具家族快速索引" diff --git "a/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/l5-mcp-index.md" "b/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/l5-mcp-index.md" new file mode 100644 index 0000000000..77565fe646 --- /dev/null +++ "b/assets/prompt-hooks/l5-mcp-\345\267\245\345\205\267-quick-index/l5-mcp-index.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/.template-ref" "b/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/.template-ref" new file mode 100644 index 0000000000..8380b9acfa --- /dev/null +++ "b/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l6-capability-wakeup.md diff --git "a/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/hook.yaml" "b/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/hook.yaml" new file mode 100644 index 0000000000..a21e3bf3c9 --- /dev/null +++ "b/assets/prompt-hooks/l6-\350\203\275\345\212\233\345\224\244\351\206\222\346\214\207\345\215\227/hook.yaml" @@ -0,0 +1,24 @@ +id: L6 +name: 能力唤醒指南 +stage: session-init +order: 600 +version: 1 +enabled: true + +# Content resolution +template: l6-capability-wakeup.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "场景到 skill 的触发反射映射" diff --git "a/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/.template-ref" "b/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/.template-ref" new file mode 100644 index 0000000000..1f80df0f22 --- /dev/null +++ "b/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/l7-collaboration-philosophy.md diff --git "a/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/hook.yaml" "b/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/hook.yaml" new file mode 100644 index 0000000000..7d88449a46 --- /dev/null +++ "b/assets/prompt-hooks/l7-\345\215\217\344\275\234\345\223\262\345\255\246/hook.yaml" @@ -0,0 +1,24 @@ +id: L7 +name: 协作哲学 +stage: session-init +order: 700 +version: 1 +enabled: true + +# Content resolution +template: l7-collaboration-philosophy.md + +# Dependencies +inputs: + [] + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "伙伴猫不是工具猫" diff --git "a/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/.template-ref" "b/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/.template-ref" new file mode 100644 index 0000000000..fd8d3340c2 --- /dev/null +++ "b/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/n1-navigation.md diff --git "a/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/hook.yaml" "b/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/hook.yaml" new file mode 100644 index 0000000000..562c4e9a45 --- /dev/null +++ "b/assets/prompt-hooks/n1-\345\257\274\350\210\252\344\270\212\344\270\213\346\226\207/hook.yaml" @@ -0,0 +1,25 @@ +id: N1 +name: 导航上下文 +stage: per-turn +order: 2400 +version: 1 +enabled: true + +# Content resolution +template: n1-navigation.md +resolver: N1NavigationResolver + +# Dependencies +inputs: + - navigationContext + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "当前的导航和定位上下文" diff --git "a/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/hook.yaml" "b/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/hook.yaml" new file mode 100644 index 0000000000..3ef59a2cd6 --- /dev/null +++ "b/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/hook.yaml" @@ -0,0 +1,25 @@ +id: R1 +name: 路由组装(串行) +stage: per-turn +order: 2200 +version: 1 +enabled: true + +# Content resolution +template: r1-route-serial.md +resolver: R1RouteSerialResolver + +# Dependencies +inputs: + - routeSerialContext + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "串行路由的组装逻辑" diff --git "a/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/r1-route-serial.md" "b/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/r1-route-serial.md" new file mode 100644 index 0000000000..23987c25f0 --- /dev/null +++ "b/assets/prompt-hooks/r1-\350\267\257\347\224\261\347\273\204\350\243\205-\344\270\262\350\241\214/r1-route-serial.md" @@ -0,0 +1,4 @@ + + + +[Route Assembly: serial mode] diff --git "a/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/hook.yaml" "b/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/hook.yaml" new file mode 100644 index 0000000000..a20abec1ec --- /dev/null +++ "b/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/hook.yaml" @@ -0,0 +1,25 @@ +id: R2 +name: 路由组装(并行) +stage: per-turn +order: 2300 +version: 1 +enabled: true + +# Content resolution +template: r2-route-parallel.md +resolver: R2RouteParallelResolver + +# Dependencies +inputs: + - routeParallelContext + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "并行路由的组装逻辑" diff --git "a/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/r2-route-parallel.md" "b/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/r2-route-parallel.md" new file mode 100644 index 0000000000..bcc4570663 --- /dev/null +++ "b/assets/prompt-hooks/r2-\350\267\257\347\224\261\347\273\204\350\243\205-\345\271\266\350\241\214/r2-route-parallel.md" @@ -0,0 +1,4 @@ + + + +[Route Assembly: parallel mode] diff --git "a/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/.template-ref" "b/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/.template-ref" new file mode 100644 index 0000000000..1937c5e3c9 --- /dev/null +++ "b/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s1-identity.md diff --git "a/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/hook.yaml" "b/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/hook.yaml" new file mode 100644 index 0000000000..ef3e197a5e --- /dev/null +++ "b/assets/prompt-hooks/s1-\350\272\253\344\273\275\345\243\260\346\230\216/hook.yaml" @@ -0,0 +1,27 @@ +id: S1 +name: 身份声明 +stage: session-init +order: 800 +version: 1 +enabled: true + +# Content resolution +template: s1-identity.md +resolver: S1IdentityResolver + +# Dependencies +inputs: + - displayName + - roleDescription + - identity + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "猫的身份、角色和昵称" diff --git "a/assets/prompt-hooks/s10-\346\212\244\346\240\217/hook.yaml" "b/assets/prompt-hooks/s10-\346\212\244\346\240\217/hook.yaml" new file mode 100644 index 0000000000..721f45ee12 --- /dev/null +++ "b/assets/prompt-hooks/s10-\346\212\244\346\240\217/hook.yaml" @@ -0,0 +1,25 @@ +id: S10 +name: 护栏 +stage: session-init +order: 1700 +version: 1 +enabled: true + +# Content resolution +template: s10-guardrails.md +resolver: S10GuardrailsResolver + +# Dependencies +inputs: + - guardrails + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "安全护栏和行为边界" diff --git "a/assets/prompt-hooks/s10-\346\212\244\346\240\217/s10-guardrails.md" "b/assets/prompt-hooks/s10-\346\212\244\346\240\217/s10-guardrails.md" new file mode 100644 index 0000000000..daa273852a --- /dev/null +++ "b/assets/prompt-hooks/s10-\346\212\244\346\240\217/s10-guardrails.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/hook.yaml" "b/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/hook.yaml" new file mode 100644 index 0000000000..a612d429c3 --- /dev/null +++ "b/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/hook.yaml" @@ -0,0 +1,25 @@ +id: S11 +name: 默认行为 +stage: session-init +order: 1800 +version: 1 +enabled: true + +# Content resolution +template: s11-defaults.md +resolver: S11DefaultsResolver + +# Dependencies +inputs: + - defaults + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "未明确指定时的默认行为设置" diff --git "a/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/s11-defaults.md" "b/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/s11-defaults.md" new file mode 100644 index 0000000000..8971b0678c --- /dev/null +++ "b/assets/prompt-hooks/s11-\351\273\230\350\256\244\350\241\214\344\270\272/s11-defaults.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/.template-ref" "b/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/.template-ref" new file mode 100644 index 0000000000..0a1a4b9d9b --- /dev/null +++ "b/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s12-world-driver.md diff --git "a/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/hook.yaml" "b/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/hook.yaml" new file mode 100644 index 0000000000..c251378005 --- /dev/null +++ "b/assets/prompt-hooks/s12-\344\270\226\347\225\214\351\251\261\345\212\250/hook.yaml" @@ -0,0 +1,25 @@ +id: S12 +name: 世界驱动 +stage: session-init +order: 1900 +version: 1 +enabled: true + +# Content resolution +template: s12-world-driver.md +resolver: S12WorldDriverResolver + +# Dependencies +inputs: + - worldDriver + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "世界观和环境驱动的行为设置" diff --git "a/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/.template-ref" "b/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/.template-ref" new file mode 100644 index 0000000000..9b127d481f --- /dev/null +++ "b/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/mcp-tools.md diff --git "a/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/hook.yaml" "b/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/hook.yaml" new file mode 100644 index 0000000000..ca36734116 --- /dev/null +++ "b/assets/prompt-hooks/s13-mcp-\345\267\245\345\205\267\346\226\207\346\241\243/hook.yaml" @@ -0,0 +1,25 @@ +id: S13 +name: MCP 工具文档 +stage: session-init +order: 2000 +version: 1 +enabled: true + +# Content resolution +template: mcp-tools.md +resolver: S13McpToolsResolver + +# Dependencies +inputs: + - mcpDocs + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: editable +transparencyTier: opt-in-view +governanceTier: auto-evolve + +# CVO-facing +userExplanation: "MCP 工具的使用文档和示例" diff --git "a/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/.template-ref" "b/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/.template-ref" new file mode 100644 index 0000000000..a85109c568 --- /dev/null +++ "b/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s2-restrictions.md diff --git "a/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/hook.yaml" "b/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/hook.yaml" new file mode 100644 index 0000000000..76d6444422 --- /dev/null +++ "b/assets/prompt-hooks/s2-\351\231\220\345\210\266\345\243\260\346\230\216/hook.yaml" @@ -0,0 +1,25 @@ +id: S2 +name: 限制声明 +stage: session-init +order: 900 +version: 1 +enabled: true + +# Content resolution +template: s2-restrictions.md +resolver: S2RestrictionsResolver + +# Dependencies +inputs: + - restrictions + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "猫不能做什么(如暹罗猫禁止写代码)" diff --git "a/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/hook.yaml" "b/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/hook.yaml" new file mode 100644 index 0000000000..2fa55f321b --- /dev/null +++ "b/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/hook.yaml" @@ -0,0 +1,25 @@ +id: S3 +name: Pack Mask(能力覆盖) +stage: session-init +order: 1000 +version: 1 +enabled: true + +# Content resolution +template: s3-pack-mask.md +resolver: S3PackMaskResolver + +# Dependencies +inputs: + - packMasks + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "Pack 级别的能力遮罩声明" diff --git "a/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/s3-pack-mask.md" "b/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/s3-pack-mask.md" new file mode 100644 index 0000000000..b925bf0ab3 --- /dev/null +++ "b/assets/prompt-hooks/s3-pack-mask-\350\203\275\345\212\233\350\246\206\347\233\226/s3-pack-mask.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/hook.yaml" "b/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/hook.yaml" new file mode 100644 index 0000000000..db9a20adbc --- /dev/null +++ "b/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/hook.yaml" @@ -0,0 +1,25 @@ +id: S4 +name: 协作格式 +stage: session-init +order: 1100 +version: 1 +enabled: true + +# Content resolution +template: s4-collaboration-format.md +resolver: S4CollabFormatResolver + +# Dependencies +inputs: + - collaborationStyle + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "猫猫之间的协作格式要求" diff --git "a/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/s4-collaboration-format.md" "b/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/s4-collaboration-format.md" new file mode 100644 index 0000000000..cde09ca3a6 --- /dev/null +++ "b/assets/prompt-hooks/s4-\345\215\217\344\275\234\346\240\274\345\274\217/s4-collaboration-format.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/.template-ref" "b/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/.template-ref" new file mode 100644 index 0000000000..688e3ebf96 --- /dev/null +++ "b/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s5-teammate-roster.md diff --git "a/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/hook.yaml" "b/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/hook.yaml" new file mode 100644 index 0000000000..c66e0d5af3 --- /dev/null +++ "b/assets/prompt-hooks/s5-\351\230\237\345\217\213\350\212\261\345\220\215\345\206\214/hook.yaml" @@ -0,0 +1,25 @@ +id: S5 +name: 队友花名册 +stage: session-init +order: 1200 +version: 1 +enabled: true + +# Content resolution +template: s5-teammate-roster.md +resolver: S5TeammateRosterResolver + +# Dependencies +inputs: + - teamStrengths + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "所有队友的能力、昵称和注意事项" diff --git "a/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/.template-ref" "b/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/.template-ref" new file mode 100644 index 0000000000..466c3306a5 --- /dev/null +++ "b/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/workflow-triggers.yaml diff --git "a/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/hook.yaml" "b/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/hook.yaml" new file mode 100644 index 0000000000..d14be21df2 --- /dev/null +++ "b/assets/prompt-hooks/s6-\345\267\245\344\275\234\346\265\201\350\247\246\345\217\221\347\202\271/hook.yaml" @@ -0,0 +1,25 @@ +id: S6 +name: 工作流触发点 +stage: session-init +order: 1300 +version: 1 +enabled: true + +# Content resolution +template: workflow-triggers.yaml +resolver: S6WorkflowTriggersResolver + +# Dependencies +inputs: + - workflowTriggers + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: editable +transparencyTier: visible-by-default +governanceTier: auto-evolve + +# CVO-facing +userExplanation: "完成某步后主动 @ 下一步猫的触发点列表" diff --git "a/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/.template-ref" "b/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/.template-ref" new file mode 100644 index 0000000000..ec606d5cff --- /dev/null +++ "b/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s7-pack-workflows.md diff --git "a/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/hook.yaml" "b/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/hook.yaml" new file mode 100644 index 0000000000..5c5aeb2edc --- /dev/null +++ "b/assets/prompt-hooks/s7-pack-\345\267\245\344\275\234\346\265\201/hook.yaml" @@ -0,0 +1,25 @@ +id: S7 +name: Pack 工作流 +stage: session-init +order: 1400 +version: 1 +enabled: true + +# Content resolution +template: s7-pack-workflows.md +resolver: S7PackWorkflowsResolver + +# Dependencies +inputs: + - packWorkflows + +# Override constraints +disableable: true + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: human-gated + +# CVO-facing +userExplanation: "Pack 特有的工作流步骤" diff --git "a/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/hook.yaml" "b/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/hook.yaml" new file mode 100644 index 0000000000..d7847adcad --- /dev/null +++ "b/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/hook.yaml" @@ -0,0 +1,25 @@ +id: S8 +name: 铲屎官参考 +stage: session-init +order: 1500 +version: 1 +enabled: true + +# Content resolution +template: s8-co-creator-reference.md +resolver: S8CoCreatorRefResolver + +# Dependencies +inputs: + - coCreatorReference + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable + +# CVO-facing +userExplanation: "铲屎官的偏好和沟通风格" diff --git "a/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/s8-co-creator-reference.md" "b/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/s8-co-creator-reference.md" new file mode 100644 index 0000000000..83d2e580a7 --- /dev/null +++ "b/assets/prompt-hooks/s8-\351\223\262\345\261\216\345\256\230\345\217\202\350\200\203/s8-co-creator-reference.md" @@ -0,0 +1,2 @@ + + diff --git "a/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/.template-ref" "b/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/.template-ref" new file mode 100644 index 0000000000..5e3384f5c2 --- /dev/null +++ "b/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/.template-ref" @@ -0,0 +1 @@ +# Phase 1 template: assets/prompt-templates/s9-governance-digest.md diff --git "a/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/hook.yaml" "b/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/hook.yaml" new file mode 100644 index 0000000000..de5c7063e3 --- /dev/null +++ "b/assets/prompt-hooks/s9-\346\262\273\347\220\206\346\221\230\350\246\201/hook.yaml" @@ -0,0 +1,25 @@ +id: S9 +name: 治理摘要 +stage: session-init +order: 1600 +version: 1 +enabled: true + +# Content resolution +template: s9-governance-digest.md +resolver: S9GovernanceDigestResolver + +# Dependencies +inputs: + - governanceDigest + +# Override constraints +disableable: false + +# Classification (Phase 1 3-axis) +safetyTier: readonly +transparencyTier: opt-in-view +governanceTier: immutable + +# CVO-facing +userExplanation: "当前的治理规则和决策记录摘要" diff --git a/assets/prompt-templates/d4-cross-thread-reply.md b/assets/prompt-templates/d4-cross-thread-reply.md index d54a3844aa..42c2519621 100644 --- a/assets/prompt-templates/d4-cross-thread-reply.md +++ b/assets/prompt-templates/d4-cross-thread-reply.md @@ -1,7 +1,8 @@ - + 📨 来自跨线程消息(source thread: {{SOURCE_THREAD}},发件猫: @{{SENDER_CAT}}){{EFFECT_LABEL}} 回复请用 cross_post_message(threadId="{{SOURCE_THREAD}}", targetCats=["{{SENDER_CAT}}"]) 本 thread 的 @{{SENDER_CAT}} 不会路由回对方(对方 session 在另一个 thread) +{{CONSTRAINT_TEXT}} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 41f29627e2..a0ebe5c465 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -77,9 +77,10 @@ created: 2026-02-26 | F231 | 启动胶囊 — per-user 画像注入与 L0 分层(猫醒来第一眼看到主人:profile capsule + relationship primer + breed/instance/user/relationship 四层,养成护城河机制本体)| in-progress | Ragdoll Fable-5 | internal (operator 2026-06-11 "我同意立项的") | [F231](features/F231-user-profile-capsule.md) | | F232 | Thread Artifacts Panel — Thread 产物视图(点开 thread 就看到它产生的所有产物:图/文件/代码PR/语音聚合 + 类型筛选 + 搜索 + 跳回原消息;A thread 内抽屉先行,B 全局产物中心未来扩展)| spec | Ragdoll Opus-4.8 | internal (operator 2026-06-11 "我觉得ok了 你立项") | [F232](features/F232-thread-artifacts-panel.md) | | F233 | Ball Custody Observability — 球权保管链可观测(值班简报:operator 收件箱+死球/睡美人/虚空告警,异常优先;feat 轨迹下钻;安乐死通道;A 简报 MVP / B 心跳+探针回执 / C 安乐死+轨迹)| in-progress(A✅ B✅ 2026-06-18 · C pending operator signoff)| Ragdoll Fable-5(spec)→ opus 家族(实现) | internal (operator 2026-06-12 "走起!喵"+"① 立项") | [F233](features/F233-ball-custody-observability.md) | -| F240 | IM Connector Plugin Architecture — 统一 IMConnectorPlugin 接口 + 外部 npm 包动态加载 | spec | Ragdoll Opus-4.6 | internal | [F240](features/F240-im-connector-plugin-architecture.md) | | F235 | Feedback-to-Community Publisher — 一键发布反馈到社区(F222 confirmed issue / 猫猫整理的问题 → 预览脱敏 → GitHub issue,补 F168 outbound 方向) | spec | Ragdoll | internal (operator 2026-06-15) | [F235](features/F235-feedback-to-community-publisher.md) | | F236 | Anchor-First Context 入口 — 返回侧 token 减负(实时 MCP 协作工具 anchor 化:指针+预览,全文按需 drill;与 F148 消息侧成版图,companion ADR-203) | spec | Ragdoll (Ragdoll Opus-4.8) + Maine Coon (gpt-5.5) | internal (operator 2026-06-15) | [F236](features/F236-anchor-first-context-entry.md) | +| F237 | Prompt Injection Visibility — 系统 prompt 注入可见性(Phase 1 生命周期查看器+模板提取+3 段编辑器 PR#859 done;Phase 2 hook pipeline migration + trace bridging in-progress;Phase 2-D override store deferred to PR 3)| in-progress (Phase 1 ✅ Phase 2 🚧) | Ragdoll Opus 4.6 | internal (operator 2026-06-02) | [F237](features/F237-prompt-injection-visibility.md) | +| F240 | IM Connector Plugin Architecture — 统一 IMConnectorPlugin 接口 + 外部 npm 包动态加载 | spec | Ragdoll Opus-4.6 | internal | [F240](features/F240-im-connector-plugin-architecture.md) | | F241 | Agent Provider Plugin / Hostable Provider Runtime — 可插拔 Agent Provider 扩展面(F143 host 契约 × F202 plugin 发现/config/UI;接入 private agent / clowder-code,不再为每个 provider 改 core) | accepted feature anchor | Community (彭潇 / `bouillipx`) + Cat Cafe maintainer guard | internal (operator 2026-06-18) + community [#941](https://github.com/zts212653/clowder-ai/issues/941) accepted 2026-06-18 | [F241](features/F241-agent-provider-plugin.md) | | F192-sop-wiring | `eval:sop` live publish path wired + re-enabled (PR #2186 merged 2026-06-10) | ✅ done | Ragdoll | internal | [F192 § 2026-06-10 timeline](features/F192-socio-technical-harness-eval.md) | | F242 | Code Graph Layer Spike — 内生「约定层关联图」(Phase A/B spike 已落 main:convention-graph package + discovery skill + deer-flow skeleton;operator 2026-06-18 撤回 full close:仍缺猫猫认知路径 / 可用入口 / 更新或重建索引行为;进入 Phase C productization gate) | in-progress (spike done) | Maine Coon (gpt-5.5) + opus-48 design | internal | [F242](features/F242-code-graph-layer-spike.md) | diff --git a/docs/features/F237-prompt-injection-visibility.md b/docs/features/F237-prompt-injection-visibility.md index 14a2917a20..f9c583b14c 100644 --- a/docs/features/F237-prompt-injection-visibility.md +++ b/docs/features/F237-prompt-injection-visibility.md @@ -1,15 +1,16 @@ --- feature_ids: [F237] related_features: [F203, F153, F180, F190, F199, F206] -topics: [system-prompt, injection, visibility, console, settings, trust, governance] +topics: [system-prompt, injection, visibility, console, settings, trust, governance, hook-pipeline, lifecycle, trace] doc_kind: spec created: 2026-06-02 -updated: 2026-06-22 +updated: 2026-06-25 +user_journey_exempt: "Phase 2 is internal pipeline migration (HookRegistry, HookPipeline, resolvers). User-visible Console UI comes in Phase 3." --- # Prompt Injection Visibility -> **Status**: done — clowder-ai#859 absorbed via cat-cafe#2505 squash `b859eb38` (2026-06-22) | **Owner**: community @mindfn + cat-cafe maintainers +> **Status**: Phase 1 done (clowder-ai#859 via cat-cafe#2505 `b859eb38`) · Phase 2 design in progress | **Owner**: Ragdoll Opus 4.6 > **Issue**: [#839](https://github.com/zts212653/clowder-ai/issues/839) > **Feature ID**: F237 (assigned by maintainer; branch/PR retain original naming) @@ -123,12 +124,669 @@ Modal showing assembled prompt per cat, labeled "approximate". Selectable by cat - [x] AC-9: Per-cat dimension selector - [x] AC-10: Malformed YAML overlay graceful fallback +## What — Phase 2: Hook Pipeline + Injection Trace + +> **Upstream status**: PROPOSAL — not yet accepted by maintainer. Upstream accepted scope is PR #859 (Phase 1) + #983 (hook demotion). This Phase 2 design requires a new pitch on issue #839 to get lifecycle abstraction accepted. The ACs and landing order below describe the proposed implementation, contingent on upstream alignment. See [Upstream Pitch Strategy](#upstream-pitch-strategy-issue-839) for how we plan to address the maintainer's prior concerns. + +### Motivation + +Phase 1 delivered visibility — operators can see what's injected. Phase 2 makes **46 content segments** self-contained, dynamically manageable, observable, and versionable via a hook pipeline. The remaining 3 segments (N2 conversation history delta, M1-M2 transport-layer) have observe-only trace adapter APIs (code + unit tests delivered); production call-site wiring is deferred due to execution order constraints (N2 assembled after trace collection point; M1-M2 assembled in invocation layer). Together, the design targets 49 of 52 segments traceable — 46 via pipeline, 3 via observe-only adapters once wired. (H1-H3 Claude Code hooks are out of Phase 2/3 scope — they use a completely different injection system and will be tracked separately.) + +**Why 46:** All segments that follow the condition → content → inject pattern become full hooks. This includes: +- **S/D segments** (34): the original `if/push` patterns in `SystemPromptBuilder.ts` +- **L1-L7** (7): dynamically compiled from template files by `compile-system-prompt-l0.mjs` at runtime — NOT build-time static. Same template → render → inject pattern as S-segments, just delivered via native L0 channel for native providers +- **B1, C1** (2): session bootstrap and MCP callback — standard condition → content pattern +- **R1-R2, N1** (3): route-layer assembly and navigation context + +**Why 3 stay observe-only:** +- **N2** (conversation history delta): immutable data assembly (`trigger: always`, `disableable: false`, `governanceTier: immutable`). Just "previous unread messages" — no customization value in hook-ifying it +- **M1-M2** (transport-layer): deliberately outside content pipeline to preserve the produced-vs-delivered boundary + +**Out of Phase 2/3 scope:** +- **H1-H3** (Claude Code hooks): completely different injection system (`.claude/hooks/` shell scripts triggered by Claude Code lifecycle events — SessionStart, PostCompact, SessionStop). Injection via event stdout → tool_result, not content pipeline. H3 explicitly "不进 model prompt". These are managed by Claude Code's hook infrastructure, not Cat Cafe's content pipeline — tracked separately as **F237-H** (to be filed as issue; dependency: Phase 2 delivers trace infrastructure that H1-H3 observability can reuse) + +> **Scope note:** The original motivating incident (opus47 dragged off-task by startup hook) may involve H1 (SessionStart hook). Phase 2 addresses Cat Cafe content pipeline visibility (49/52 segments). If the incident trigger was an H1-H3 hook, full closure requires F237-H delivery. Phase 2's trace schema and persistence layer are designed to be reusable by F237-H + +### Why Hook Pipeline + +The current `SystemPromptBuilder` assembles segments via manual `if/push` patterns: + +```typescript +/* @segment D5 */ if (context.pingPongWarning) { + const d5 = renderSegment('D5', vars); + if (d5) lines.push(d5); +} +``` + +This pattern has served well for 52 segments, but makes several operations hard: + +| Operation | Current Cost | With Hook Pipeline | +|-----------|-------------|-------------------| +| Disable a segment | Find code, comment out, deploy | `enabled: false` in hook.yaml, deploy | +| Try a new version | Branch + code change + PR | Add v2 template, switch version in hook.yaml | +| Roll back | Revert commit + deploy | Revert version in hook.yaml | +| Know what fired | Read source + infer from logs | InjectionTrace record per turn | +| Add a new segment | Write code + template + manifest + tests | Template + manifest entry | +| Remove a segment | Find and delete code + template | `enabled: false`, then delete at leisure | + +**This is not "freezing dynamic injections into static claims"** — this is making dynamic injections *declaratively manageable*. The YAML manifest describes registration metadata (stage, enabled, version, dependencies), not content policy. Content lives in templates and code resolvers, exactly as today. The difference: lifecycle operations (enable/disable/version/trace) become data operations, not code operations. + +**Why this makes Build-to-Delete easier, not harder**: The maintainer's concern was that metadata turns deletion into deprecation. The opposite is true — currently, deleting a segment requires finding all code paths (condition, variable setup, render call, push), verifying no side effects, removing the template, updating the manifest display entry, and testing. With hooks: set `enabled: false`, the segment stops firing immediately. The code and template can be deleted at leisure in a cleanup pass, or left dormant with zero runtime cost. Build-to-Delete becomes a config toggle followed by optional cleanup. + +**Why this is the foundation for "injections grow from trajectories"**: The maintainer wants injections to grow organically from per-user taste, cross-thread repetition signal, and CVO correction. For that, the system needs to: +1. **Trace** which segments fired per turn and what content they produced +2. **Correlate** segment combinations with turn outcomes +3. **Iterate** — try new versions, compare, promote or demote + +Without a hook pipeline, there's no structured trace data, no version identity, and no way to correlate "segment X contributed to outcome Y." The hook pipeline is the measurement substrate that trajectory-based growth requires. + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ContextAssembler │ +│ (centralized IO: queries stores, builds typed AssemblerInput) │ +└──────────────────────────┬──────────────────────────────────────┘ + │ AssemblerInput + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Runtime Content Pipeline (46 hooks) │ +│ │ +│ session-init S1-S13, B1, C1, L1-L7 (identity + rules) │ +│ per-turn D1-D21, R1-R2, N1 (context + routing) │ +│ │ +│ Each hook: condition → resolve → render │ +│ → emit PromptPatch + TraceEvent │ +└──────────────────────────┬──────────────────────────────────────┘ + │ PromptPatch[] + TraceEvent[] + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Transport Assembly │ +│ (OUTSIDE pipeline — independent injection mechanics) │ +│ │ +│ injectSystemPrompt decision (resume/force/registryChanged) │ +│ stagingPrepend (ADR-038, every-turn) │ +│ contextHintPrefix (F225, every-turn) │ +│ missionPrefix (F070, external project dispatch) │ +│ M2 transcriptPathHints (always appended) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ Surface Trace Adapters (observe only — 3 segments) │ +│ │ +│ N2 conversation history delta (immutable data assembly) │ +│ M1-M2 transport-layer (missionPrefix, transcriptPathHints) │ +│ │ +│ No resolvers, no enable/disable, no versioning. │ +│ Only emit TraceEvents for observability. │ +│ │ +│ (H1-H3 Claude Code hooks: out of scope — tracked separately) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Two Tiers: Runtime Content Pipeline + Surface Trace Adapters + +Phase 2 targets 49 of 52 segments in two tiers (H1-H3 Claude Code hooks are out of scope — tracked separately). Tier 1 (46 segments) is fully operational via HookPipeline. Tier 2 (3 segments) has adapter API + unit tests; production call-site wiring is deferred (see AC-P2-13): + +**Tier 1 — Runtime Content Pipeline (46 segments)** + +These segments follow the condition → content → inject pattern and benefit from full hook-ification: manifest, resolver, versioning, trace. The pipeline has two stages (runtime override store added in PR 3): + +| Stage | When | Segments | Source | +|-------|------|----------|--------| +| `session-init` | New session / re-injection / registry change | S1-S13, B1, C1, L1-L7 (22 hooks) | `buildStaticIdentity()`, `SessionBootstrap`, `McpPromptInjector`, `compile-system-prompt-l0.mjs` | +| `per-turn` | Every invocation, before model call | D1-D21, R1-R2, N1 (24 hooks) | `buildInvocationContext()`, route layer, `route-helpers.ts` | + +Why these segments unify: +- **S1-S13, D1-D21** (34): original `if/push` patterns in `SystemPromptBuilder.ts` — the core use case +- **L1-L7** (7): dynamically compiled from `assets/prompt-templates/l*.md` template files at runtime by `compileL0()`. Same template → render → inject pattern as S-segments. Delivery channel = `native-l0` for native providers. The L0 compiler's content source is refactored: instead of independently loading template files, it consumes pipeline-produced output for L1-L7. The delivery mechanism (`--system-prompt-file`, native L0 channel) is preserved unchanged +- **B1** (1): session bootstrap — condition (new session?) → content. Joins `session-init` +- **C1** (1): MCP callback — condition (MCP available?) → content. Joins `session-init` _(`.local` overlay migration to override store deferred to PR 3)_ +- **R1-R2** (2): route assembly — condition → content at route layer. Joins `per-turn` +- **N1** (1): navigation context — condition → content. Joins `per-turn` + +Hook contract: +- **Input**: `AssemblerInput` — typed context data gathered by ContextAssembler +- **Output**: `PromptPatch` (rendered content) + `TraceEvent` (observability record) + +**Tier 2 — Surface Trace Adapters (3 segments: N2, M1-M2)** + +These segments are genuinely different from the hook pipeline pattern: + +| Surface | Source | Why observe-only | +|---------|--------|-----------------| +| N2 (1) | `route-helpers.ts` | **Immutable data assembly**. Conversation history delta = "previous unread messages." `trigger: always`, `disableable: false`, `governanceTier: immutable`. No customization value — not something you'd version, disable, or template-ify | +| M1 (1) | `invoke-single-cat.ts` | **Transport-layer** (missionPrefix, F070). Deliberately outside content pipeline to preserve produced-vs-delivered boundary | +| M2 (1) | `invoke-single-cat.ts` | **Transport-layer** (transcriptPathHints). Always appended, always delivered. Same reason as M1 | + +Total: 1 + 2 = **3 segments**. Combined with Tier 1's 46 hooks = **49 segments** in Phase 2 scope. (H1-H3, 3 segments, tracked separately — see [Out of scope](#what-phase-2-does-not-include).) + +Trace adapters have no resolvers, no enable/disable toggle, no versioning. They only produce `TraceEventObserved`. + +### HookManifest — Self-Contained Segment Definition + +Each hook is defined by a YAML manifest (registration metadata) + optional code resolver (condition + variable setup) + template file (content). Following the `PluginRegistry` pattern (F202): + +```yaml +# assets/prompt-hooks/D5-ping-pong-warning/hook.yaml +id: D5 +name: 乒乓球警告 +stage: per-turn +order: 500 # execution order within stage (see below) +version: 1 +enabled: true + +# Content resolution +template: d5-ping-pong-warning.md # existing Phase 1 template +resolver: D5PingPongResolver # code resolver class name (optional) + +# Dependencies — what AssemblerInput fields this hook reads +inputs: + - pingPongWarning # field name on AssemblerInput + +# Override constraints +disableable: true # false = override store rejects disable (S1, D8, L1-L7 etc.) + +# Classification (Phase 1 3-axis, carried forward) +safetyTier: limited-edit # readonly | limited-edit | editable — gates template override +transparencyTier: visible-by-default +governanceTier: human-gated # immutable | human-gated | auto-evolve — gates version override + +# CVO-facing +userExplanation: "当两只猫连续互传 ≥2 轮时警告,避免死循环" +``` + +**Key properties:** +- `id` — stable segment identifier (S1, D5, etc.), matches Phase 1 manifest +- `stage` — which clock signal triggers this hook +- `order` — integer, determines execution order within a stage. Lower = earlier in the compiled prompt. Built-in hooks use 100-step spacing (S1=100, S2=200, ..., D1=100, D2=200, ...) to leave room for future insertions without reordering existing hooks. The order directly maps to the current `if/push` sequence in `SystemPromptBuilder.ts` — same output order, same model behavior. Order is per-stage (session-init and per-turn each start from their own sequence). Not overridable at runtime — order is a structural property of the prompt, not a user-tunable knob +- `version` — integer, enables v1→v2 migration without deleting v1 +- `enabled` — boolean, the Build-to-Delete toggle +- `template` — path to content template (reuses Phase 1 extracted templates) +- `resolver` — optional TypeScript class that evaluates condition and prepares template variables. Hooks without a resolver are unconditional (always fire when stage fires) +- `inputs` — declares which `AssemblerInput` fields the resolver reads. Enables dependency analysis and makes each hook's data requirements explicit + +**Migration from Phase 1:** Each of the 46 pipelined segments becomes a `hook.yaml` + its existing template file. For S/D segments, the resolver code is extracted from the inline `if/push` pattern. For L1-L7, the existing template files (`l1-parallel-world.md` etc.) become hook templates; the L0 compiler's content source switches from direct template loading to pipeline-produced output (delivery channel unchanged). For B1/C1/R1-R2/N1, resolvers wrap existing execution logic. Zero content change, zero behavior change — same transformation principle as Phase 1's template extraction. The 3 observe-only segments (N2, M1-M2) are not migrated into the hook directory. (H1-H3 are out of Phase 2 scope.) + +### HookRegistry — Scan, Register, Resolve + +Modeled on `PluginRegistry` (scan directory, parse manifests, validate, derive status): + +```typescript +interface HookRegistry { + /** Scan hook directory, parse manifests, validate, register */ + scan(): HookManifest[]; + + /** Get hooks for a specific stage, ascending by `order` field */ + getStageHooks(stage: HookStage): RegisteredHook[]; + + /** Get single hook by ID */ + getHook(hookId: string): RegisteredHook | undefined; + + /** All registered hooks */ + getAllHooks(): RegisteredHook[]; + + /** Enabled state from manifest baseline */ + isEnabled(hookId: string): boolean; + + /** Active version from manifest baseline */ + getActiveVersion(hookId: string): number; + + // PR 3 adds: getEffective(hookId) → EffectiveHookState + // (runtime override ?? manifest baseline resolution chain) +} + +interface RegisteredHook { + manifest: HookManifest; // baseline, from repo YAML + resolver: HookResolver | null; // null = unconditional, always fires + template: string; // baseline template content +} +``` + +**State model — manifest baseline (PR 2) + runtime override (PR 3):** + +> _The following describes the full two-layer design. PR 2 implements manifest baseline only; the runtime override layer is deferred to PR 3._ + +Hook state uses a two-layer resolution chain: **runtime override** (Redis-persisted, PR 3) takes precedence over **manifest baseline** (git-tracked YAML, PR 2). This separates product-level concerns from user-level concerns: + +``` +Effective state = runtime override ?? manifest baseline +``` + +| Layer | What it controls | Who changes it | How | +|-------|-----------------|----------------|-----| +| **Manifest baseline** | Which hooks exist, default templates, default enabled/version | Product team | git commit + deploy | +| **Runtime override** | Enable/disable, version switch, template edits, auto-eval iterations | Operator / auto-eval | Console API / auto-eval API | + +**Why two layers:** +- **Baseline** is the product's factory default — it defines which hooks ship and their default behavior. Adding or removing a built-in hook is a product-level change that goes through git. This is the "file + git + restart" channel. +- **Runtime override** is the user's workspace — operators can disable hooks they don't want, edit templates to customize behavior, switch versions. Auto-eval (Phase 3) writes to the same override layer. Override and auto-eval use the same mechanism; there's no distinction between manual and automated customization. +- **Package-install users** never touch git. Their entire interaction is through runtime overrides on top of the shipped baseline. + +**Safety boundary — three override constraints:** + +| Override type | Gated by | Rule | +|---------------|----------|------| +| **Template edit** | `safetyTier` | `readonly` (49/52) reject, `limited-edit` / `editable` (3/52) accept | +| **Enable/disable** | `disableable` | `disableable: false` hooks (identity, safety, routing constraints) reject disable override; `disableable: true` hooks accept | +| **Version switch** | `governanceTier` | `immutable` hooks reject version override; `human-gated` / `auto-evolve` accept | + +The override store validates all three constraints before accepting a write. Attempting to disable a `disableable: false` hook (e.g., S1 identity, D8 ball ownership, L1-L7 core rules) returns an error with the constraint violation. + +**Runtime override store:** + +```typescript +interface HookOverrideStore { + /** Get override for a hook (null = use baseline) */ + getOverride(hookId: string): HookOverride | null; + + /** Set override (operator or auto-eval). Validates constraints: + * - enabled=false rejected if manifest.disableable=false + * - templateContent rejected if manifest.safetyTier='readonly' + * - version rejected if manifest.governanceTier='immutable' + * Throws OverrideConstraintError on violation. */ + setOverride(hookId: string, override: HookOverride): void; + + /** Clear override (revert to baseline) */ + clearOverride(hookId: string): void; + + /** List all active overrides */ + listOverrides(): Array<{ hookId: string; override: HookOverride }>; +} + +interface HookOverride { + enabled?: boolean; // override enabled state + version?: number; // override active version + templateContent?: string; // override template (safetyTier gated) + source: 'operator' | 'auto-eval'; + updatedAt: number; + reason?: string; // why this override was set +} +``` + +Keyed by `hook-override:{hookId}`. TTL=0 (persistent) — user customizations must survive restart (LL-048). Audit trail: each override records `source` and `reason`, enabling "who changed this and why" queries. + +**Directory structure:** + +``` +assets/prompt-hooks/ +├── S1-identity/ +│ ├── hook.yaml +│ └── s1-identity.md # existing Phase 1 template (symlink or move) +├── D5-ping-pong-warning/ +│ ├── hook.yaml +│ ├── d5-ping-pong-warning.md +│ └── d5-ping-pong-warning.v2.md # version 2 template (future) +├── D8-a2a-ball-check/ +│ ├── hook.yaml +│ └── a2a-ball-check.md +└── ... +``` + +### ContextAssembler — Centralized IO + +Today, `buildInvocationContext()` receives a 30+ field `InvocationContext` bag where each field is consumed by exactly one segment (e.g., `pingPongWarning` → D5, `crossThreadReplyHint` → D4). The data comes from route-layer queries scattered across `route-serial.ts`, `route-parallel.ts`, and `route-helpers.ts`. + +ContextAssembler centralizes this: + +```typescript +interface ContextAssembler { + /** + * Gather all inputs needed by active hooks for this stage. + * Route-layer calls this once; hooks never do their own store queries. + */ + assemble(stage: HookStage, baseContext: BaseContext): Promise; +} + +/** BaseContext = what the route layer already has (catId, threadId, userId, sessionId, etc.) */ +interface BaseContext { + catId: CatId; + threadId: string; + userId: string; + sessionId: string | null; + dispatch: EffectiveDispatch; + // ... other route-layer provided values +} + +/** AssemblerInput = typed bag of everything hooks might need */ +interface AssemblerInput extends BaseContext { + // Session-init stage inputs + catConfig: CatConfig; + mcpAvailable: boolean; + packBlocks: PackBlocks | null; + callableMentions: MentionInfo; + + // Per-turn stage inputs + directMessageFrom: CatId | null; + crossThreadReplyHint: CrossThreadHint | null; + pingPongWarning: string | null; + teammates: TeammateInfo[]; + routeMode: RouteMode; + // ... all current InvocationContext fields, typed +} +``` + +**Why centralize IO:** Hooks that query stores directly become impossible to test, trace, or mock. By gathering all inputs upfront, we get: +1. **Testability** — unit test any hook with a synthetic `AssemblerInput` +2. **Trace completeness** — the trace record can include which inputs were present +3. **Performance** — one round of queries per stage, not per hook + +### Hook Execution Model + +Each hook produces `PromptPatch` (content) + `TraceEvent` (observability). No direct context mutation in Phase 2: + +```typescript +interface HookResolver { + /** + * Evaluate whether this hook should fire and prepare template variables. + * Returns a discriminated union — no mutable state on the resolver instance, + * safe for concurrent invocations sharing a registry singleton. + */ + resolve(input: AssemblerInput): ResolveResult; +} + +type ResolveResult = + | { status: 'fired'; vars: Record; templateVersion?: number } + | { status: 'skipped'; reasonCode: string; reason: string }; + +/** What a hook produces after resolution + template rendering */ +interface PromptPatch { + hookId: string; + stage: HookStage; + content: string; // rendered template content + position: 'append'; // Phase 2: append only. Future: prepend, replace +} + +/** Discriminated union — status determines which fields are present */ +type TraceEvent = + | TraceEventFired + | TraceEventSkipped + | TraceEventDisabled + | TraceEventObserved; + +interface TraceEventBase { + hookId: string; + stage: HookStage; + durationMs: number; // resolver execution time (0 for disabled/observed) +} + +interface TraceEventFired extends TraceEventBase { + status: 'fired'; + version: number; // which version of the hook fired + contentHash: string; // SHA-256 of rendered content + tokenEstimate: number; // approx token count +} + +interface TraceEventSkipped extends TraceEventBase { + status: 'skipped'; + reasonCode: string; // e.g., "no_ping_pong_warning", "condition_false" + reason: string; // human-readable: "pingPongWarning not present" +} + +interface TraceEventDisabled extends TraceEventBase { + status: 'disabled'; + disabledBy: 'manifest' | 'operator' | 'auto-eval'; // which layer disabled it +} + +/** For Tier 2 surface trace adapters (N2, M1-M2) */ +interface TraceEventObserved extends TraceEventBase { + status: 'observed'; + contentHash: string | null; // hash of observed content, null if not available + tokenEstimate: number; +} +``` + +**Pipeline execution per stage (Tier 1 — S/D hooks only):** + +``` +for each registered hook in stage (ascending by `order` field): + 1. Check manifest baseline enabled → false? + → emit TraceEvent { status: 'disabled', disabledBy: 'manifest' } + (PR 3 adds: override ?? manifest resolution chain) + 2. If hook has resolver → call resolver.resolve(input) + - Returns { status: 'skipped', reasonCode, reason } + → emit TraceEvent { status: 'skipped', reasonCode, reason } + - Returns { status: 'fired', vars, templateVersion? } + → continue to step 4 + 3. If hook has no resolver → unconditional (always fire with empty vars) + 4. Render template with vars → PromptPatch + 5. Emit TraceEvent { status: 'fired', contentHash, tokenEstimate, version } +``` + +### InjectionTrace — Dual-Layer Persistence + +After each turn, persist injection trace data in two layers with different retention strategies: + +**Layer 1: InjectionTraceSummary (persistent)** + +Lightweight structural record — which hooks fired/skipped, aggregate stats. This is the data substrate for Phase 3 eval and long-term trend analysis. Default TTL=0 (persistent), consistent with the iron law that user-visible, traceable state defaults to persistent (LL-048). + +```typescript +interface InjectionTraceSummary { + turnId: string; + sessionId: string; + threadId: string; + catId: string; + timestamp: number; + + /** Per-hook summary, one entry per hook (fired/skipped/disabled/observed) */ + hooks: TraceEventSummary[]; + + /** Per-stage delivery decision — did produced content reach the model? */ + delivery: StageDeliveryDecision[]; + + /** Aggregate stats (of produced content, not delivered) */ + totalTokens: number; + totalHooksFired: number; + totalHooksSkipped: number; + totalDurationMs: number; +} + +/** Compact per-hook entry — no content, just identity + outcome */ +interface TraceEventSummary { + hookId: string; + status: 'fired' | 'skipped' | 'disabled' | 'observed'; + version?: number; // only for fired + tokenEstimate?: number; // only for fired/observed + reasonCode?: string; // only for skipped +} +``` + +Keyed by `injection-trace-summary:{threadId}:{turnId}`. Queryable for trend analysis: "how often does D5 fire across the last 100 turns?" + +**Layer 2: InjectionTraceDetail (debug, short TTL)** + +Full `TraceEvent` records including content hashes, durations, human-readable reasons. For debugging "what exactly happened on turn N?" Default TTL = 7 days (configurable), consistent with F153's pattern for debug-level span data. + +```typescript +interface InjectionTraceDetail { + turnId: string; + threadId: string; + catId: string; + timestamp: number; + + /** Full TraceEvent array (discriminated union, all fields) */ + hooks: TraceEvent[]; +} +``` + +Keyed by `injection-trace-detail:{threadId}:{turnId}`. + +**Why dual-layer:** Full `TraceEvent` records with content hashes and durations are valuable for debugging but expensive to store indefinitely and rarely needed after the immediate debugging window. The summary layer captures the structural signal (which hooks, what outcome) needed for eval correlation and trend analysis without storing transient debug detail. This mirrors F153's approach: structured pointers persist, debug captures expire. + +SessionContext holds only `currentTurnId` + `previousTurnId` references, not trace data itself. + +### Transport Assembly Boundary + +The following injection mechanics stay **OUTSIDE** the hook pipeline. They are transport-layer concerns, not content-production concerns: + +| Mechanism | Location | Why Outside | +|-----------|----------|-------------| +| `injectSystemPrompt` decision | `invoke-single-cat.ts:1639` | Complex resume/force/registry logic that determines WHETHER static identity is sent, not WHAT content to produce | +| `stagingPrepend` | `invoke-single-cat.ts:1674` | ADR-038 contract: "每轮注入生效", independent of prompt content | +| `contextHintPrefix` | `invoke-single-cat.ts:1661` | F225: context management, independent of prompt assembly | +| `missionPrefix` | `invoke-single-cat.ts:1650` | F070: external project dispatch context | +| `M2 transcriptPathHints` | `invoke-single-cat.ts:1680` | Always-appended path hints | + +Transport assembly order remains: `stagingPrepend → contextHintPrefix → (systemPrompt + missionPrefix + invocationContext) → M2`. + +The hook pipeline produces the **systemPrompt** (from session-init hooks) and **invocationContext** (from per-turn hooks). Transport assembly decides how to deliver them. This separation means: +- The pipeline can evolve content independently of delivery mechanics +- Transport assembly can change (e.g., new prepend layers) without touching hooks +- The `injectSystemPrompt` decision (resume vs force-reinjection) stays clean — it's a delivery decision, not a content decision + +**Produced vs Delivered — critical trace distinction:** + +Session-init hooks (S1-S13) fire inside `buildStaticIdentity()`, which runs on every invocation. But the produced content is only delivered to the model when `injectSystemPrompt` is true (new session, force-reinjection, or registry change). On resumed turns with `canSkipOnResume`, the S-segment content is produced but **not sent**. If the trace only records "S1 fired", it creates false observability — the operator sees "S1 was active this turn" when the model never received it. + +To fix this, the `InjectionTraceSummary` includes a per-stage **delivery decision** record that is channel-aware: + +```typescript +interface StageDeliveryDecision { + stage: 'session-init' | 'per-turn'; + delivered: boolean; + channel: DeliveryChannel; + reason: string; // e.g., "injectSystemPrompt=false (resume, canSkipOnResume)" +} + +type DeliveryChannel = + | 'message-prepend' // non-native: S-content prepended to prompt string + | 'native-l0' // native providers (Claude/Codex/OpenCode): L0 via --system-prompt-file / developer_instructions + | 'pack-only' // native L0 with hasNativeL0=true: only pack blocks via buildStaticIdentityPackOnly() + | 'always-delivered'; // per-turn D-segments, transport-layer M1/M2 +``` + +Delivery semantics per stage and provider: + +- **session-init (non-native providers)**: `delivered = injectSystemPrompt`, `channel = 'message-prepend'` +- **session-init (native L0 providers — Claude/Codex/OpenCode)**: S-content is delivered via native L0 channel (compiled `system-prompt-l0.md`), NOT via `injectSystemPrompt`. Route code uses `buildStaticIdentityPackOnly()` when `hasNativeL0=true` — only pack blocks go through `buildStaticIdentity()`. So `injectSystemPrompt` is not the delivery truth for native providers; the native L0 channel is. +- **per-turn**: `delivered = true`, `channel = 'always-delivered'` (D-segments are always part of the prompt regardless of provider) + +This means `TraceEventSummary` records what the pipeline *produced*, and `StageDeliveryDecision` records what transport *delivered and through which channel*. Together they answer "what did we prepare?", "what did the model actually see?", and "how was it delivered?" — the distinction needed for accurate eval correlation in Phase 3. + +For Tier 2 transport-layer adapters (M1/M2), delivery is inherent (`channel = 'always-delivered'`). L1-L7 delivery depends on whether the provider uses native L0 (`channel = 'native-l0'`) or message-prepend (`channel = 'message-prepend'`, gated by `injectSystemPrompt`). + +### Surface Trace Adapters (Tier 2 — 3 segments) + +The 3 segments that genuinely don't fit the runtime content pipeline get lightweight observe-only adapters: + +- **N2 (conversation history delta)**: Adapter at `route-helpers.ts` records conversation history assembly. Immutable, always-on, no customization value. +- **M1-M2 (transport-layer)**: M1 adapter records dispatch mission context (missionPrefix, F070). M2 adapter records transcript path hints. Both at `invoke-single-cat.ts` transport assembly point — they remain outside the content pipeline to preserve the produced-vs-delivered boundary. + +Trace adapters produce `TraceEventObserved` only — no `PromptPatch`, no enable/disable, no versioning. When fully wired, this will cover 49 of 52 segments (46 Tier 1 + 3 Tier 2). Currently: adapter API + unit tests delivered in `trace-adapters.ts`; production call-site wiring deferred (N2 content assembled after trace collection point in route-serial; M1-M2 assembled in invoke-single-cat.ts after route-level trace). (H1-H3 tracked separately.) + +### Versioning Model + +Each hook can have multiple template versions: + +``` +assets/prompt-hooks/D5-ping-pong-warning/ +├── hook.yaml # version: 2 (active) +├── d5-ping-pong-warning.md # v1 template +└── d5-ping-pong-warning.v2.md # v2 template (active) +``` + +Version lifecycle: +1. **Create** — add `hookname.v2.md` template alongside v1 +2. **Activate** — update `version: 2` in `hook.yaml` +3. **Roll back** — set `version: 1` in `hook.yaml` +4. **Archive** — delete old version template when confident + +The resolver receives the active version and renders the corresponding template. TraceEvent records which version fired, enabling comparison of v1 vs v2 outcomes. + +### What Phase 2 Does NOT Include + +- **H1-H3 Claude Code hooks** — completely different injection system (`.claude/hooks/` shell scripts, triggered by Claude Code lifecycle events, injected via event stdout → tool_result). Not part of Cat Cafe's content pipeline. Tracked as **F237-H** (separate issue to be filed). Dependency: F237-H can reuse Phase 2's trace schema and persistence layer +- **Eval feedback loop** — automated analysis of trace data to score/iterate segments. This is Phase 3, consuming Phase 2's trace + override infrastructure +- **Context mutation** — hooks producing side effects beyond PromptPatch (e.g., modifying session state). Future capability tier +- **Custom user hooks** — operators can't register their own hooks yet. This requires security model design beyond Phase 2's scope +- **L0 delivery channel modification** — the native L0 delivery mechanism (`--system-prompt-file`, provider-specific channel) is unchanged. The pipeline replaces the L0 compiler's *content source* (templates → pipeline-produced output) but preserves its *delivery path*. See L1-L7 architecture notes above + +### Landing Order + +Phase 2 implementation in 5 sub-phases, each independently shippable: + +| Sub-phase | Deliverable | Tests | +|-----------|------------|-------| +| **P2-A: HookManifest + Registry** | Hook YAML schema for all 46 pipelined segments, directory scan, manifest parsing. Registry lists S1-S13, B1, C1, L1-L7, D1-D21, R1-R2, N1 | Schema validation tests, scan tests (following PluginRegistry test pattern) | +| **P2-B: ContextAssembler + Resolvers** | Extract resolver logic: S/D from `if/push` patterns, L1-L7 from L0 compiler templates, B1/C1/R/N1 wrapping existing execution points. ContextAssembler gathers inputs. Dual-path: old code path + new pipeline produce identical output. L0 compiler content source switched from direct template loading to pipeline-produced output (delivery channel unchanged) | Snapshot tests: old output === new output for all 46 hooks. L0 compiled output equivalence test | +| **P2-C: Pipeline Execution + Trace Adapters** | Wire HookPipeline into session-init and per-turn stages. Remove old patterns. Add Tier 2 trace adapter API for N2 + M1-M2 (3 observe-only; adapter code + unit tests delivered, production call-site wiring deferred — execution order constraint) | Integration tests: compiled output identical. Regression: all existing tests pass. Trace adapter unit tests | +| **P2-D: Runtime Override Store** _(deferred to PR 3)_ | Redis-backed override layer (`HookOverrideStore`). Console UI: enable/disable hooks, switch versions, edit templates (safetyTier-gated). Overrides persist across restart (TTL=0). Same write API for operator and future auto-eval | Override resolution tests (override ?? baseline). Safety tier gate tests. Persistence tests | +| **P2-E: InjectionTrace Persistence** | Dual-layer persistence (summary persistent + detail short TTL). Console trace viewer. Trace records fired/skipped/disabled status per hook | Trace record completeness tests. Console: can view which hooks fired per turn _(override source tracking deferred to PR 3)_ | + +## Acceptance Criteria — Phase 2 + +- [ ] AC-P2-1: HookManifest YAML schema defined for S/D segments, validated by `check-hook-manifest.mjs` +- [ ] AC-P2-2: HookRegistry scans `assets/prompt-hooks/`, parses all 46 hook manifests (S1-S13, B1, C1, L1-L7, D1-D21, R1-R2, N1) +- [ ] AC-P2-3: ContextAssembler produces typed `AssemblerInput` from route-layer queries for session-init and per-turn stages +- [ ] AC-P2-4: 46 content hooks have standalone resolvers (S/D from `if/push`, L1-L7 from L0 compiler templates, B1/C1/R/N1 wrapping existing execution points); N2 + M1-M2 have observe-only trace adapter API (see AC-P2-13 for wiring status) +- [ ] AC-P2-5: Dual-path validation: old `if/push` output === new pipeline output for all S/D segments (snapshot tests) +- [ ] AC-P2-6: `buildStaticIdentity()` and `buildInvocationContext()` delegate to HookPipeline +- [ ] AC-P2-7: Each S/D hook execution produces TraceEvent (discriminated union: fired/skipped/disabled) +- [ ] AC-P2-8: InjectionTraceSummary persisted per turn (persistent, TTL=0); InjectionTraceDetail persisted with configurable TTL (default 7 days) +- [ ] AC-P2-8a: InjectionTraceSummary includes per-stage `StageDeliveryDecision` with channel-aware delivery truth: `message-prepend` gated by `injectSystemPrompt`, `native-l0` gated by provider native channel, `always-delivered` for per-turn/transport, `pack-only` for native L0 pack blocks +- [ ] AC-P2-9: Console trace viewer: query which hooks fired per turn per thread +- [ ] AC-P2-10: Hook versioning: v1→v2 switch via manifest baseline, TraceEvent records version _(runtime override switching deferred to PR 3)_ +- [ ] AC-P2-11: Hook enable/disable via manifest baseline (`enabled` field), TraceEvent records disabled status _(runtime override gating + constraint violation deferred to PR 3)_ +- [ ] AC-P2-12: Transport assembly (staging/contextHint/missionPrefix/M2) unchanged, not in pipeline +- [ ] AC-P2-13: Tier 2 trace adapter API (`observeN2`/`observeM1`/`observeM2` in `trace-adapters.ts`) emits `TraceEventObserved` for N2 + M1-M2 — adapter code + unit tests delivered; production call-site wiring deferred (N2 assembled after trace collection; M1-M2 in invocation layer after route-level trace) +- [ ] AC-P2-14: Zero behavior change — compiled prompt output identical pre/post migration (with no overrides active) +- [ ] AC-P2-14a: L0 compiled output equivalence — `compile-system-prompt-l0.mjs` output identical when consuming pipeline-produced L1-L7 content vs direct template loading +- [ ] AC-P2-15: _(deferred to PR 3)_ Runtime override store (Redis, TTL=0) with two-layer resolution: override ?? manifest baseline +- [ ] AC-P2-16: _(deferred to PR 3)_ Template override gated by safetyTier — readonly hooks reject template writes, limited-edit/editable hooks accept +- [ ] AC-P2-17: _(deferred to PR 3)_ Override audit trail: each override records source (operator/auto-eval), timestamp, reason +- [ ] AC-P2-18: _(deferred to PR 3)_ Override constraint enforcement — `setOverride` rejects: disable on `disableable: false` hooks, template edit on `safetyTier: readonly` hooks, version switch on `governanceTier: immutable` hooks. Returns `OverrideConstraintError` with violated constraint + +## Upstream Strategy (Issue #839) + +### Agreed Dual-Track Approach (2026-06-25) + +Maintainer accepted our path analysis. Agreed sequencing: + +**Fork (internal development):** Build full pipeline + trace as Path B — pipeline produces TraceEvents as natural output. No throwaway instrumentation. This becomes the working prototype. + +**Upstream (PR sequence to clowder-ai):** + +| PR | Content | Dependency | +|----|---------|-----------| +| **PR 1: InjectionTrace v0** | Trace schema + lightweight instrumentation on current `if/push` + persistence + Console viewer. Zero behavior change. | None | +| **PR 2: Pipeline migration** | Hook manifests + resolvers + pipeline switchover. Informed by PR 1 trace data + fork prototype. Equivalence proof: ordering, conditions, native L0, transport boundaries. | PR 1 merged + trace data | +| **PR 3: Override store** | Runtime override layer, auth model, auto-eval writeback. Separate design review. | PR 2 merged | + +**Rationale:** Maintainer wants upstream to stay low-risk — first PR should not commit the main repo to the hook abstraction before trace data and a reviewed migration argument exist. Fork development avoids throwaway work internally. + +### Maintainer Concerns (addressed in proposal comment) + +### Concern 1: "Schema-driven catalog compresses dynamic injections into static YAML claims" + +**Our response:** The hook manifest describes *registration metadata* (which stage, whether enabled, what version), not *content policy*. Content lives in templates and code resolvers, exactly as today. The manifest is the "phone book" — it says where to reach each hook, not what the hook should say. This is the same relationship as `PluginRegistry` (F202) — plugin manifests describe capabilities, not behavior. + +### Concern 2: "Pre-freezing N-stage pipeline locks the extension surface" + +**Our response:** Phase 2 defines 2 runtime pipeline stages (session-init, per-turn), not the 8-10 stage lifecycle from the original proposal. These stages are the minimal observation boundary around existing execution points — they formalize what's already there, not invent new boundaries. Only 3 segments (N2, M1-M2) get observe-only trace adapters — the rest all join the pipeline. (H1-H3 Claude Code hooks are out of Phase 2 scope entirely.) Adding a hook to a stage doesn't require a new interface; the stages are registration categories, not extension APIs. + +### Concern 3: "Build-to-Delete — metadata turns deletion into deprecation" + +**Our response:** The opposite. Currently, deleting a segment requires: find all code paths → remove condition + vars + render + push → verify no side effects → remove template → update manifest display entry → test. With hooks: set `enabled: false` in the hook's YAML manifest, commit, deploy — segment stops firing immediately. The resolver code and template can be deleted at leisure in a cleanup pass. The manifest file itself is just a YAML file in git — deleting the hook directory removes it completely. Build-to-Delete becomes: disable → observe trace confirms no regressions → delete directory. No "deprecation" metadata survives deletion. + +### Concern 4: "Injections grow from trajectories, not pre-numbered interfaces" + +**Our response:** The hook pipeline IS the substrate for trajectory-based growth. For injections to "grow from real trajectories," the system needs: (1) structured trace data showing which segments fired per turn, (2) correlation between segment combinations and outcomes, (3) versioning to A/B test segment changes. Without a pipeline, there's no measurement infrastructure. Phase 2 delivers the measurement; Phase 3 (eval feedback loop) delivers the iteration. + +### Issue #983 (Hook Output Demotion) + +Separately accepted upstream. Not blocked by Phase 2 — can land independently as a behavioral fix within the current `if/push` code. Phase 2 would make it a hook-level concern (resolver checks dispatch priority). + ## Future Work -- Hook output dispatch-aware demotion (separate behavioral PR) +- Hook output dispatch-aware demotion (#983, separate behavioral PR) - Text deduplication across A2A routing sections - Preview accuracy improvements (native-L0 routing, pack blocks, C1 overlays) -- Manifest documentation refinements (concrete source paths, H1/H3 readonly marking) +- Manifest documentation refinements (concrete source paths) +- **H1-H3 Claude Code hooks (F237-H)** — separate tracking for observability/iteration of external hook injection system. Reuses Phase 2 trace schema/persistence. Required to fully close original motivating incident if trigger was H1-related +- **Phase 3: Eval Feedback Loop** — automated trace analysis, segment scoring, A/B version comparison +- **Custom User Hooks** — operator-registered hooks with security sandboxing +- **Context Mutation Hooks** — hooks that modify session state (requires safety model design) ## Dependencies @@ -136,3 +794,18 @@ Modal showing assembled prompt per cat, labeled "approximate". Selectable by cat - **Related**: F153 (tracing — future observability integration) - **Related**: F180 (hook health/sync) - **Related**: F190/F199/F206 (Console settings infrastructure) + +## Timeline + +| Date | Event | +|------|-------| +| 2026-06-02 | Kickoff: motivating incident analysis + CVO direction | +| 2026-06-02 | Issue #839 created, maintainer triage | +| 2026-06-03 | CVO approved Phase 1, worktree created | +| 2026-06-04-10 | Implementation: 6 rounds of codex local review | +| 2026-06-11 | Gate passed (build + tsc + test + lint), PR #859 opened | +| 2026-06-11-12 | Cloud review: 34 findings processed (1 fixed, 33 pushback) | +| 2026-06-15 | Scope discussion with maintainer on #839 | +| 2026-06-16 | PR #859 merged, Phase 1 complete | +| 2026-06-24 | Phase 2 design: hook pipeline + injection trace spec | +| 2026-06-25 | Phase 2 design review passed (codex R1: 3 P1 + 1 P2 fixed) | diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/attribution.json b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/attribution.json new file mode 100644 index 0000000000..c8218e2ad7 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/attribution.json @@ -0,0 +1,11 @@ +{ + "verdictId": "2026-07-09-eval-a2a-restart-clean-keep-observe", + "featureId": "F167", + "evalSnapshotId": "eval-F167-2026-07-09", + "generatedAt": "2026-07-09T03:02:43.611Z", + "findings": [], + "noFindingRecord": { + "reason": "No friction signals detected across 5 components", + "evidence": "Checked components: L1, C1, C2, route-serial, grounding-phase-o. Friction metrics examined: c1.hold_zombie_count, c1.hold_cancel_count, c2.verdict_without_pass_count, c2.void_hold_hint_emitted, inline_action.feedback_written, inline_action.hint_emitted, grounding.budget_exhausted_total. All values within threshold." + } +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/provenance.json b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/provenance.json new file mode 100644 index 0000000000..21c89a27b8 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/provenance.json @@ -0,0 +1,19 @@ +{ + "verdictId": "2026-07-09-eval-a2a-restart-clean-keep-observe", + "rawInputs": [ + { + "path": "docs/harness-feedback/snapshots/2026-07-09-F167-eval.yaml", + "sha256": "d16607f63746714b582ff7aea4949ab95f7171c1d341bc3c148600dbb113ab17" + }, + { + "path": "docs/harness-feedback/attributions/2026-07-09-F167-attribution.yaml", + "sha256": "1d3bcd3be304eec7d0771fa918ef531bb9282951da68a4362188de372177d98e" + } + ], + "generatedAt": "2026-07-09T03:02:43.611Z", + "generator": { + "name": "eval-a2a-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f192-e-pilot-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/snapshot.json b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/snapshot.json new file mode 100644 index 0000000000..319db1581a --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-a2a-restart-clean-keep-observe/snapshot.json @@ -0,0 +1,84 @@ +{ + "verdictId": "2026-07-09-eval-a2a-restart-clean-keep-observe", + "evalSnapshotId": "eval-F167-2026-07-09", + "featureId": "F167", + "generatedAt": "2026-07-09T03:02:43.609Z", + "window": { + "startMs": 1783488221877, + "endMs": 1783566052518, + "durationHours": 21.6196225 + }, + "counterWindow": { + "startMs": 1783498472883, + "endMs": 1783566163554, + "durationHours": 18.802964052719723 + }, + "components": [ + { + "id": "L1", + "name": "WorklistRegistry (ping-pong breaker)", + "confidence": "medium", + "activationCounts": { + "l1.streak_warn_count": 0, + "l1.streak_break_count": 0 + }, + "frictionCounts": {} + }, + { + "id": "C1", + "name": "hold_ball (MCP tool)", + "confidence": "medium", + "activationCounts": { + "hold_ball_calls": 0, + "c1.hold_replacement_count": 0 + }, + "frictionCounts": { + "c1.hold_zombie_count": 0, + "c1.hold_cancel_count": 0 + } + }, + { + "id": "C2", + "name": "exit-check (forced-pass guard)", + "confidence": "medium", + "activationCounts": { + "hint_emitted (mixed routing+verdict)": 1, + "c2.verdict_hint_emitted": 0, + "c2.checked": 61, + "c2.void_hold_checked": 61 + }, + "frictionCounts": { + "c2.verdict_without_pass_count": 0, + "c2.void_hold_hint_emitted": 0 + } + }, + { + "id": "route-serial", + "name": "route-serial (A2A handoff routing)", + "confidence": "high", + "activationCounts": { + "inline_action.checked": 61, + "inline_action.detected": 1, + "line_start.detected": 8 + }, + "frictionCounts": { + "inline_action.feedback_written": 1, + "inline_action.hint_emitted": 1 + } + }, + { + "id": "grounding-phase-o", + "name": "claim grounding (Phase O shadow)", + "confidence": "medium", + "activationCounts": { + "grounding.check_total": 0, + "grounding.verdict_total": 0, + "grounding.resolver_total": 0, + "grounding.cache_hit_total": 0 + }, + "frictionCounts": { + "grounding.budget_exhausted_total": 0 + } + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/attribution.json b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/attribution.json new file mode 100644 index 0000000000..7d00c04ba6 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/attribution.json @@ -0,0 +1,36 @@ +{ + "verdictId": "2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only", + "featureId": "F245", + "evalSnapshotId": "eval-F245-2026-07-09", + "generatedAt": "2026-07-09T03:09:18.602Z", + "findings": [ + { + "id": "FR-2026-07-09-d981127d413a", + "relatedFeature": "F245", + "frictionSignal": { + "type": "friction.cluster_d981127d413a", + "severity": "high", + "confidence": 0.7, + "detectedAt": "2026-07-09T03:09:18.602Z" + }, + "attribution": { + "primaryLayer": "needs_investigation", + "evidence": [ + { + "type": "counter", + "anchor": "friction-rollup/cluster_d981127d413a", + "excerpt": "cluster 'a2a_timeout: codex' count=1 severity=high sensorForms=[reason]" + } + ] + }, + "proposedAction": [ + { + "action": "triage-top-friction-cluster", + "target": "F245/friction-rollup", + "rationale": "Highest-ranked friction cluster in the window — eval cat assigns the 7-class root cause in the verdict before handoff." + } + ], + "status": "open" + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/provenance.json b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/provenance.json new file mode 100644 index 0000000000..c751f6d94d --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/provenance.json @@ -0,0 +1,15 @@ +{ + "verdictId": "2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only", + "rawInputs": [ + { + "path": "docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/raw/rollup-report.json", + "sha256": "2152e3490b03474074c42fc4d3433908f52dc8aeddc04de378b228bb21c0a25e" + } + ], + "generatedAt": "2026-07-09T03:09:18.602Z", + "generator": { + "name": "eval-friction-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f245-friction-rollup-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/raw/rollup-report.json b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/raw/rollup-report.json new file mode 100644 index 0000000000..fe978ec290 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/raw/rollup-report.json @@ -0,0 +1,313 @@ +{ + "verdictId": "2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only", + "selector": { + "kind": "friction-rollup-snapshot", + "windowStartMs": 1783306800000, + "windowEndMs": 1783566000000, + "topN": 10, + "tokenCap": 4000 + }, + "window": { + "sinceMs": 1783306800000, + "untilMs": 1783566000000 + }, + "signalCount": 9, + "clusterCount": 5, + "degraded": false, + "droppedChannels": [], + "report": { + "window": { + "sinceMs": 1783306800000, + "untilMs": 1783566000000 + }, + "generatedAt": "2026-07-09T03:09:18.602Z", + "topClusters": [ + { + "clusterId": "d981127d413a", + "representative": "a2a_timeout: codex", + "channels": [ + "user-feedback" + ], + "count": 1, + "members": [ + { + "signalId": "user-feedback:fi_mrcu4bxlv840fgsx", + "rawRef": "fi_mrcu4bxlv840fgsx", + "channel": "user-feedback" + } + ], + "method": "rule", + "sensorForms": [ + "reason" + ], + "severity": "high" + }, + { + "clusterId": "23fba4045727", + "representative": "c2.void_hold_hint_emitted=6", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#C2#c2.void_hold_hint_emitted", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#C2#c2.void_hold_hint_emitted", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#C2#c2.void_hold_hint_emitted", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#C2#c2.void_hold_hint_emitted", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low" + }, + { + "clusterId": "4901ab640cdd", + "representative": "inline_action.hint_emitted=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.hint_emitted", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.hint_emitted", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.hint_emitted", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.hint_emitted", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low" + }, + { + "clusterId": "9d32022cb1ab", + "representative": "inline_action.feedback_written=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.feedback_written", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.feedback_written", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.feedback_written", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.feedback_written", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low" + }, + { + "clusterId": "a60da62bea20", + "representative": "inline_action.routed_set_skip=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.routed_set_skip", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.routed_set_skip", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.routed_set_skip", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.routed_set_skip", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low" + } + ], + "actionableCandidates": [ + { + "clusterId": "d981127d413a", + "representative": "a2a_timeout: codex", + "channels": [ + "user-feedback" + ], + "count": 1, + "members": [ + { + "signalId": "user-feedback:fi_mrcu4bxlv840fgsx", + "rawRef": "fi_mrcu4bxlv840fgsx", + "channel": "user-feedback" + } + ], + "method": "rule", + "sensorForms": [ + "reason" + ], + "severity": "high", + "actionability": "actionable_candidate", + "followupDraft": { + "clusterId": "d981127d413a", + "title": "Investigate friction cluster: a2a_timeout: codex", + "summary": "a2a_timeout: codex", + "evidenceRefs": [ + "fi_mrcu4bxlv840fgsx" + ], + "reportingMode": "final-only" + }, + "referenceOnlyEvidenceRefs": [] + } + ], + "referenceOnly": [ + { + "clusterId": "23fba4045727", + "representative": "c2.void_hold_hint_emitted=6", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#C2#c2.void_hold_hint_emitted", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#C2#c2.void_hold_hint_emitted", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#C2#c2.void_hold_hint_emitted", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#C2#c2.void_hold_hint_emitted", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low", + "actionability": "reference_only", + "evidenceRefs": [ + "2026-07-06-eval-a2a-ordinary-monitoring#C2#c2.void_hold_hint_emitted", + "2026-07-07-eval-a2a-stable-void-hold-baseline#C2#c2.void_hold_hint_emitted" + ] + }, + { + "clusterId": "4901ab640cdd", + "representative": "inline_action.hint_emitted=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.hint_emitted", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.hint_emitted", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.hint_emitted", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.hint_emitted", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low", + "actionability": "reference_only", + "evidenceRefs": [ + "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.hint_emitted", + "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.hint_emitted" + ] + }, + { + "clusterId": "9d32022cb1ab", + "representative": "inline_action.feedback_written=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.feedback_written", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.feedback_written", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.feedback_written", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.feedback_written", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low", + "actionability": "reference_only", + "evidenceRefs": [ + "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.feedback_written", + "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.feedback_written" + ] + }, + { + "clusterId": "a60da62bea20", + "representative": "inline_action.routed_set_skip=1", + "channels": [ + "eval-domain" + ], + "count": 2, + "members": [ + { + "signalId": "eval-domain:2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.routed_set_skip", + "rawRef": "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.routed_set_skip", + "channel": "eval-domain" + }, + { + "signalId": "eval-domain:2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.routed_set_skip", + "rawRef": "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.routed_set_skip", + "channel": "eval-domain" + } + ], + "method": "rule", + "sensorForms": [ + "aggregate_proxy" + ], + "severity": "low", + "actionability": "reference_only", + "evidenceRefs": [ + "2026-07-06-eval-a2a-ordinary-monitoring#route-serial#inline_action.routed_set_skip", + "2026-07-07-eval-a2a-stable-void-hold-baseline#route-serial#inline_action.routed_set_skip" + ] + } + ], + "tailSummary": { + "clusterCount": 0, + "signalCount": 0, + "byChannel": {} + }, + "degraded": false, + "droppedChannels": [], + "tokenBudget": { + "cap": 4000, + "estimated": 1798 + } + } +} diff --git a/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/snapshot.json b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/snapshot.json new file mode 100644 index 0000000000..c0d97f371b --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/snapshot.json @@ -0,0 +1,30 @@ +{ + "verdictId": "2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only", + "evalSnapshotId": "eval-F245-2026-07-09", + "featureId": "F245", + "generatedAt": "2026-07-09T03:09:18.602Z", + "window": { + "startMs": 1783306800000, + "endMs": 1783566000000, + "durationHours": 72 + }, + "components": [ + { + "id": "friction-rollup", + "name": "friction rollup (Top-N + sensorForm)", + "confidence": "medium", + "activationCounts": {}, + "frictionCounts": { + "cluster_count": 5, + "top_cluster_count": 5, + "tail_cluster_count": 0, + "tail_signal_count": 0, + "cluster_d981127d413a": 1, + "cluster_23fba4045727": 2, + "cluster_4901ab640cdd": 2, + "cluster_9d32022cb1ab": 2, + "cluster_a60da62bea20": 2 + } + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/attribution.json b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/attribution.json new file mode 100644 index 0000000000..329415b841 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/attribution.json @@ -0,0 +1,11 @@ +{ + "verdictId": "2026-07-11-eval-a2a-low-traffic-clean-keep-observe", + "featureId": "F167", + "evalSnapshotId": "eval-F167-2026-07-11", + "generatedAt": "2026-07-11T03:14:24.367Z", + "findings": [], + "noFindingRecord": { + "reason": "No friction signals detected across 5 components", + "evidence": "Checked components: L1, C1, C2, route-serial, grounding-phase-o. Friction metrics examined: c1.hold_zombie_count, c1.hold_cancel_count, c2.verdict_without_pass_count, c2.void_hold_hint_emitted, grounding.budget_exhausted_total. All values within threshold." + } +} diff --git a/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/provenance.json b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/provenance.json new file mode 100644 index 0000000000..203ea1e18e --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/provenance.json @@ -0,0 +1,19 @@ +{ + "verdictId": "2026-07-11-eval-a2a-low-traffic-clean-keep-observe", + "rawInputs": [ + { + "path": "docs/harness-feedback/snapshots/2026-07-11-eval-F167-runtime.yaml", + "sha256": "13bd70769d3a142fdad138296d30bbff2e60b43981ad5e43bd1c80e73d6cc294" + }, + { + "path": "docs/harness-feedback/attributions/2026-07-11-eval-F167-attribution.yaml", + "sha256": "717830c400462712c7d9e4a995aacf3ca2f4e3310796f532c0131bc5ef51e5d7" + } + ], + "generatedAt": "2026-07-11T03:14:24.367Z", + "generator": { + "name": "eval-a2a-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f192-e-pilot-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/snapshot.json b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/snapshot.json new file mode 100644 index 0000000000..aa6bf96220 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/snapshot.json @@ -0,0 +1,82 @@ +{ + "verdictId": "2026-07-11-eval-a2a-low-traffic-clean-keep-observe", + "evalSnapshotId": "eval-F167-2026-07-11", + "featureId": "F167", + "generatedAt": "2026-07-11T03:14:24.366Z", + "window": { + "startMs": 1783693354481, + "endMs": 1783739419707, + "durationHours": 12.79589611111111 + }, + "counterWindow": { + "startMs": 1783692875048, + "endMs": 1783739664363, + "durationHours": 12.997031821955833 + }, + "components": [ + { + "id": "L1", + "name": "WorklistRegistry (ping-pong breaker)", + "confidence": "medium", + "activationCounts": { + "l1.streak_warn_count": 0, + "l1.streak_break_count": 0 + }, + "frictionCounts": {} + }, + { + "id": "C1", + "name": "hold_ball (MCP tool)", + "confidence": "medium", + "activationCounts": { + "hold_ball_calls": 0, + "c1.hold_replacement_count": 0 + }, + "frictionCounts": { + "c1.hold_zombie_count": 0, + "c1.hold_cancel_count": 0 + } + }, + { + "id": "C2", + "name": "exit-check (forced-pass guard)", + "confidence": "medium", + "activationCounts": { + "hint_emitted (mixed routing+verdict)": null, + "c2.verdict_hint_emitted": 0, + "c2.checked": 16, + "c2.void_hold_checked": 16 + }, + "frictionCounts": { + "c2.verdict_without_pass_count": 0, + "c2.void_hold_hint_emitted": 0 + } + }, + { + "id": "route-serial", + "name": "route-serial (A2A handoff routing)", + "confidence": "high", + "activationCounts": { + "line_start.detected": 7, + "inline_action.checked": 16 + }, + "frictionCounts": {} + }, + { + "id": "grounding-phase-o", + "name": "claim grounding (Phase O shadow)", + "confidence": "medium", + "activationCounts": { + "grounding.check_total": 1, + "grounding.verdict_total": 1, + "grounding.resolver_total": 0, + "grounding.cache_hit_total": 0, + "grounding.sample_count": 2, + "grounding.mismatch_sample_count": 0 + }, + "frictionCounts": { + "grounding.budget_exhausted_total": 0 + } + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/attribution.json b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/attribution.json new file mode 100644 index 0000000000..f41d27c4eb --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/attribution.json @@ -0,0 +1,11 @@ +{ + "verdictId": "2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe", + "featureId": "F167", + "evalSnapshotId": "eval-F167-2026-07-12", + "generatedAt": "2026-07-12T03:02:17.575Z", + "findings": [], + "noFindingRecord": { + "reason": "No friction signals detected across 5 components", + "evidence": "Checked components: L1, C1, C2, route-serial, grounding-phase-o. Friction metrics examined: c1.hold_zombie_count, c1.hold_cancel_count, c2.verdict_without_pass_count, c2.void_hold_hint_emitted, inline_action.feedback_written, grounding.budget_exhausted_total. All values within threshold." + } +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/provenance.json b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/provenance.json new file mode 100644 index 0000000000..dff627433b --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/provenance.json @@ -0,0 +1,19 @@ +{ + "verdictId": "2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe", + "rawInputs": [ + { + "path": "docs/harness-feedback/snapshots/2026-07-12-eval-F167-runtime.yaml", + "sha256": "f27fee15da592959c211471f118db3ad29804ceed7f8d9363d07e72d3bccd441" + }, + { + "path": "docs/harness-feedback/attributions/2026-07-12-eval-F167-attribution.yaml", + "sha256": "1f9218cd63ab45006242f15e95a296dd3d2dd53319f76609af9ed0d7b2ee48df" + } + ], + "generatedAt": "2026-07-12T03:02:17.575Z", + "generator": { + "name": "eval-a2a-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f192-e-pilot-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/snapshot.json b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/snapshot.json new file mode 100644 index 0000000000..4c2c6f884e --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/snapshot.json @@ -0,0 +1,85 @@ +{ + "verdictId": "2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe", + "evalSnapshotId": "eval-F167-2026-07-12", + "featureId": "F167", + "generatedAt": "2026-07-12T03:02:17.574Z", + "window": { + "startMs": 1783739935067, + "endMs": 1783825316085, + "durationHours": 23.716949444444445 + }, + "counterWindow": { + "startMs": 1783781681401, + "endMs": 1783825337571, + "durationHours": 12.126713968345 + }, + "components": [ + { + "id": "L1", + "name": "WorklistRegistry (ping-pong breaker)", + "confidence": "medium", + "activationCounts": { + "l1.streak_warn_count": 0, + "l1.streak_break_count": 0 + }, + "frictionCounts": {} + }, + { + "id": "C1", + "name": "hold_ball (MCP tool)", + "confidence": "medium", + "activationCounts": { + "hold_ball_calls": 0, + "c1.hold_replacement_count": 0 + }, + "frictionCounts": { + "c1.hold_zombie_count": 0, + "c1.hold_cancel_count": 0 + } + }, + { + "id": "C2", + "name": "exit-check (forced-pass guard)", + "confidence": "medium", + "activationCounts": { + "hint_emitted (mixed routing+verdict)": null, + "c2.verdict_hint_emitted": 0, + "c2.checked": 108, + "c2.void_hold_checked": 109 + }, + "frictionCounts": { + "c2.verdict_without_pass_count": 0, + "c2.void_hold_hint_emitted": 0 + } + }, + { + "id": "route-serial", + "name": "route-serial (A2A handoff routing)", + "confidence": "high", + "activationCounts": { + "inline_action.checked": 109, + "line_start.detected": 55, + "inline_action.detected": 1 + }, + "frictionCounts": { + "inline_action.feedback_written": 1 + } + }, + { + "id": "grounding-phase-o", + "name": "claim grounding (Phase O shadow)", + "confidence": "medium", + "activationCounts": { + "grounding.check_total": 2, + "grounding.verdict_total": 2, + "grounding.resolver_total": 0, + "grounding.cache_hit_total": 0, + "grounding.sample_count": 4, + "grounding.mismatch_sample_count": 0 + }, + "frictionCounts": { + "grounding.budget_exhausted_total": 0 + } + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/attribution.json b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/attribution.json new file mode 100644 index 0000000000..6c813e1e23 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/attribution.json @@ -0,0 +1,47 @@ +{ + "verdictId": "2026-07-12-eval-anchor-first-c1-keep-observe", + "featureId": "F236", + "evalSnapshotId": "eval-F236-2026-07-12", + "generatedAt": "2026-07-12T03:16:06.684Z", + "findings": [ + { + "id": "AF-2026-07-12-thread-context", + "relatedFeature": "F236", + "sunsetSignals": { + "anchorTax": false, + "highOpenRate": false, + "netNegative": false + }, + "frictionSignal": { + "type": "anchor.open_rate.thread-context", + "severity": "low", + "confidence": 0.8, + "detectedAt": "2026-07-12T03:16:06.684Z" + }, + "attribution": { + "primaryLayer": "needs_investigation", + "evidence": [ + { + "type": "counter", + "anchor": "anchor-telemetry-rollup/thread-context_drilled_unique_items", + "excerpt": "thread-context: 1/110 items drilled (0.9% open rate), charsSaved=280650, drillChars=1453, netBenefit=279197" + } + ] + }, + "proposedAction": [ + { + "action": "keep-observe", + "target": "anchor-telemetry-rollup/thread-context", + "rationale": "Open rate 0.9%: 1/110 items drilled, net benefit 279197 chars" + } + ] + } + ], + "sunsetAssessment": { + "toolCount": 1, + "toolsWithAnchorTax": 0, + "toolsNetNegative": 0, + "toolsHighOpenRate": 0, + "lowSampleToolCount": 0 + } +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/provenance.json b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/provenance.json new file mode 100644 index 0000000000..8fbb77a1b3 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/provenance.json @@ -0,0 +1,15 @@ +{ + "verdictId": "2026-07-12-eval-anchor-first-c1-keep-observe", + "rawInputs": [ + { + "path": "docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/raw/rollup.json", + "sha256": "e6d7fd49e6e2c1b2858988d69156324fd70a93b4233ed0b8e0d6afe3d19842d8" + } + ], + "generatedAt": "2026-07-12T03:16:06.684Z", + "generator": { + "name": "eval-anchor-first-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f236-anchor-telemetry-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/raw/rollup.json b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/raw/rollup.json new file mode 100644 index 0000000000..409dcc2f3e --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/raw/rollup.json @@ -0,0 +1,49 @@ +{ + "verdictId": "2026-07-12-eval-anchor-first-c1-keep-observe", + "selector": { + "kind": "anchor-telemetry-snapshot", + "windowStartMs": 1783739721102, + "windowEndMs": 1783826121102 + }, + "rollup": { + "perTool": { + "thread-context": { + "previewResponses": 16, + "previewedItems": 110, + "drills": 1, + "drilledUniqueItems": 1, + "openRateByItem": 0.00909090909090909, + "returnedChars": 35478, + "originalChars": 316128, + "charsSaved": 280650, + "drillChars": 1453, + "netBenefit": 279197 + } + }, + "orphanDrills": 0, + "adoption": { + "explicitAnchorCalls": 2, + "explicitFullCalls": 3, + "defaultAnchorCalls": 14, + "defaultFullCalls": 0, + "legacyEquivalentAnchorCalls": 0, + "legacyEquivalentFullCalls": 1, + "unknownModeCalls": 0, + "uniqueCatsExplicitAnchor": 1 + }, + "track1Snapshot": { + "returnedByTool": { + "thread-context": 16 + }, + "returnedCharsByTool": { + "thread-context": 85572 + }, + "drillByTool": { + "get-message": 1 + }, + "drillCharsByTool": { + "get-message": 1453 + } + } + } +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/snapshot.json b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/snapshot.json new file mode 100644 index 0000000000..af84fa5a6b --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-anchor-first-c1-keep-observe/snapshot.json @@ -0,0 +1,39 @@ +{ + "verdictId": "2026-07-12-eval-anchor-first-c1-keep-observe", + "evalSnapshotId": "eval-F236-2026-07-12", + "featureId": "F236", + "generatedAt": "2026-07-12T03:16:06.684Z", + "window": { + "startMs": 1783739721102, + "endMs": 1783826121102, + "durationHours": 24 + }, + "components": [ + { + "id": "anchor-telemetry-rollup", + "name": "anchor-first preview/drill open-rate rollup", + "confidence": "medium", + "activationCounts": { + "orphan_drills": 0, + "adoption_explicit_anchor_calls": 2, + "adoption_explicit_full_calls": 3, + "adoption_default_anchor_calls": 14, + "adoption_default_full_calls": 0, + "adoption_legacy_equivalent_anchor_calls": 0, + "adoption_legacy_equivalent_full_calls": 1, + "adoption_unique_cats_explicit_anchor": 1, + "adoption_unknown_mode_calls": 0, + "thread-context_preview_responses": 16, + "thread-context_previewed_items": 110, + "thread-context_drills": 1, + "thread-context_drilled_unique_items": 1, + "thread-context_returned_chars": 35478, + "thread-context_original_chars": 316128, + "thread-context_chars_saved": 280650, + "thread-context_drill_chars": 1453, + "thread-context_net_benefit": 279197 + }, + "frictionCounts": {} + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/attribution.json b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/attribution.json new file mode 100644 index 0000000000..4907f9f384 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/attribution.json @@ -0,0 +1,36 @@ +{ + "verdictId": "2026-07-12-eval-friction-singleton-user-feedback-improved-baseline", + "featureId": "F245", + "evalSnapshotId": "eval-F245-2026-07-12", + "generatedAt": "2026-07-12T03:03:25.003Z", + "findings": [ + { + "id": "FR-2026-07-12-4e7626161f05", + "relatedFeature": "F245", + "frictionSignal": { + "type": "friction.cluster_4e7626161f05", + "severity": "medium", + "confidence": 0.7, + "detectedAt": "2026-07-12T03:03:25.003Z" + }, + "attribution": { + "primaryLayer": "needs_investigation", + "evidence": [ + { + "type": "counter", + "anchor": "friction-rollup/cluster_4e7626161f05", + "excerpt": "cluster 'text_frustration: 错了 什么情况' count=1 severity=medium sensorForms=[reason]" + } + ] + }, + "proposedAction": [ + { + "action": "triage-top-friction-cluster", + "target": "F245/friction-rollup", + "rationale": "Highest-ranked friction cluster in the window — eval cat assigns the 7-class root cause in the verdict before handoff." + } + ], + "status": "open" + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/provenance.json b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/provenance.json new file mode 100644 index 0000000000..ebb9971f53 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/provenance.json @@ -0,0 +1,15 @@ +{ + "verdictId": "2026-07-12-eval-friction-singleton-user-feedback-improved-baseline", + "rawInputs": [ + { + "path": "docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/raw/rollup-report.json", + "sha256": "127da312b1484ce1b22f1541389fc0c9cf80e2a41ccbcd403aa4d29075e620e7" + } + ], + "generatedAt": "2026-07-12T03:03:25.003Z", + "generator": { + "name": "eval-friction-live-verdict", + "version": "1" + }, + "sanitizeRulesVersion": "f245-friction-rollup-v1" +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/raw/rollup-report.json b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/raw/rollup-report.json new file mode 100644 index 0000000000..d036305a81 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/raw/rollup-report.json @@ -0,0 +1,92 @@ +{ + "verdictId": "2026-07-12-eval-friction-singleton-user-feedback-improved-baseline", + "selector": { + "kind": "friction-rollup-snapshot", + "windowStartMs": 1783566000000, + "windowEndMs": 1783825200000, + "topN": 10, + "tokenCap": 4000 + }, + "window": { + "sinceMs": 1783566000000, + "untilMs": 1783825200000 + }, + "signalCount": 1, + "clusterCount": 1, + "degraded": false, + "droppedChannels": [], + "report": { + "window": { + "sinceMs": 1783566000000, + "untilMs": 1783825200000 + }, + "generatedAt": "2026-07-12T03:03:25.003Z", + "topClusters": [ + { + "clusterId": "4e7626161f05", + "representative": "text_frustration: 错了 什么情况", + "channels": [ + "user-feedback" + ], + "count": 1, + "members": [ + { + "signalId": "user-feedback:fi_mrg2nhcf21wi0xzq", + "rawRef": "fi_mrg2nhcf21wi0xzq", + "channel": "user-feedback" + } + ], + "method": "rule", + "sensorForms": [ + "reason" + ], + "severity": "medium" + } + ], + "actionableCandidates": [ + { + "clusterId": "4e7626161f05", + "representative": "text_frustration: 错了 什么情况", + "channels": [ + "user-feedback" + ], + "count": 1, + "members": [ + { + "signalId": "user-feedback:fi_mrg2nhcf21wi0xzq", + "rawRef": "fi_mrg2nhcf21wi0xzq", + "channel": "user-feedback" + } + ], + "method": "rule", + "sensorForms": [ + "reason" + ], + "severity": "medium", + "actionability": "actionable_candidate", + "followupDraft": { + "clusterId": "4e7626161f05", + "title": "Investigate friction cluster: text_frustration: 错了 什么情况", + "summary": "text_frustration: 错了 什么情况", + "evidenceRefs": [ + "fi_mrg2nhcf21wi0xzq" + ], + "reportingMode": "final-only" + }, + "referenceOnlyEvidenceRefs": [] + } + ], + "referenceOnly": [], + "tailSummary": { + "clusterCount": 0, + "signalCount": 0, + "byChannel": {} + }, + "degraded": false, + "droppedChannels": [], + "tokenBudget": { + "cap": 4000, + "estimated": 296 + } + } +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/snapshot.json b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/snapshot.json new file mode 100644 index 0000000000..5f3fd5d4da --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/snapshot.json @@ -0,0 +1,26 @@ +{ + "verdictId": "2026-07-12-eval-friction-singleton-user-feedback-improved-baseline", + "evalSnapshotId": "eval-F245-2026-07-12", + "featureId": "F245", + "generatedAt": "2026-07-12T03:03:25.003Z", + "window": { + "startMs": 1783566000000, + "endMs": 1783825200000, + "durationHours": 72 + }, + "components": [ + { + "id": "friction-rollup", + "name": "friction rollup (Top-N + sensorForm)", + "confidence": "medium", + "activationCounts": {}, + "frictionCounts": { + "cluster_count": 1, + "top_cluster_count": 1, + "tail_cluster_count": 0, + "tail_signal_count": 0, + "cluster_4e7626161f05": 1 + } + } + ] +} diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/attribution.json b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/attribution.json new file mode 100644 index 0000000000..745601fec8 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/attribution.json @@ -0,0 +1,11 @@ +{ + "verdictId": "2026-07-12-eval-qc-zero-baseline-w28", + "featureId": "F253", + "evalSnapshotId": "qc-snapshot-2026-07-12-eval-qc-zero-baseline-w28", + "generatedAt": "2026-07-12T03:07:54.524Z", + "findings": [], + "noFindingRecord": { + "reason": "Zero-baseline bootstrap (Phase C) — no live QC data sources wired", + "evidence": "All QC metrics are zero (prCount=0). Eval:qc data pipeline not yet active." + } +} \ No newline at end of file diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/provenance.json b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/provenance.json new file mode 100644 index 0000000000..244c54d100 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/provenance.json @@ -0,0 +1,15 @@ +{ + "verdictId": "2026-07-12-eval-qc-zero-baseline-w28", + "rawInputs": [ + { + "path": "bundles/2026-07-12-eval-qc-zero-baseline-w28/snapshot.json", + "sha256": "4235c7eb9bb3e203ad46425451f5e2766546a24ede6f831759e96d4605a36c9d" + } + ], + "generatedAt": "2026-07-12T03:07:54.524Z", + "generator": { + "name": "qc-generator-adapter", + "version": "1.0.0" + }, + "sanitizeRulesVersion": "1.0.0" +} \ No newline at end of file diff --git a/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/snapshot.json b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/snapshot.json new file mode 100644 index 0000000000..25773b06c1 --- /dev/null +++ b/docs/harness-feedback/bundles/2026-07-12-eval-qc-zero-baseline-w28/snapshot.json @@ -0,0 +1,27 @@ +{ + "verdictId": "2026-07-12-eval-qc-zero-baseline-w28", + "evalSnapshotId": "qc-snapshot-2026-07-12-eval-qc-zero-baseline-w28", + "featureId": "F253", + "generatedAt": "2026-07-12T03:07:54.524Z", + "window": { + "startMs": 1783209600000, + "endMs": 1783814400000, + "durationHours": 168 + }, + "components": [ + { + "componentId": "qc-pipeline", + "componentName": "QC Pipeline", + "activationCounts": { + "finding_yield": 0, + "reviewer_delta": 0, + "pr_count": 0 + }, + "frictionCounts": { + "false_positive_rate": 0, + "post_merge_bug_rate": 0 + }, + "confidence": "no-data" + } + ] +} \ No newline at end of file diff --git a/docs/harness-feedback/verdicts/2026-07-09-eval-a2a-restart-clean-keep-observe.md b/docs/harness-feedback/verdicts/2026-07-09-eval-a2a-restart-clean-keep-observe.md new file mode 100644 index 0000000000..9a1e3c0225 --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-09-eval-a2a-restart-clean-keep-observe.md @@ -0,0 +1,35 @@ +--- +feature_ids: [F192, F167] +topics: [harness-eval, eval-a2a, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:a2a +packet_id: 2026-07-09-eval-a2a-restart-clean-keep-observe +source_snapshot: "snapshot:bundle/2026-07-09-eval-a2a-restart-clean-keep-observe/snapshot" +--- + +# Live Verdict — 2026-07-09-eval-a2a-restart-clean-keep-observe + +- Verdict: `keep_observe` +- Phenomenon: The 2026-07-09 window is clean on current counters: C2 forced-pass is 0/61 and void-hold is 0/61, with no C1 zombie-hold finding and no grounding mismatches. Trend interpretation is downgraded because the API process restarted during the trace window: trace coverage is 21.62h, but counter_window is 18.80h and the C2 denominator is far below the prior high-traffic windows. +- Harness: F167/C2 (A2A Chain Quality runtime harness: exit-check / void-hold / grounding shadow telemetry) +- Owner ask: No code action from this 7/9 packet. Continue ordinary eval:a2a monitoring; if the next high-denominator window returns void_hold above roughly 2.5% or keeps per-fire sample coverage below 80%, promote a separate F167 tracer coverage build ask before the 5% floor is crossed. +- Re-eval: Keep ordinary monitoring while verdict_without_pass stays at zero or isolated low counts, void_hold remains below the 5% floor, and grounding mismatch_sample_count remains zero. Reopen as build if a full counter window repeats the 7/8 drift shape or if sample coverage remains below 80% on recurring void_hold emissions. at 2026-07-10T03:01:28Z + +Evidence: +- snapshot:bundle/2026-07-09-eval-a2a-restart-clean-keep-observe/snapshot +- attribution:bundle/2026-07-09-eval-a2a-restart-clean-keep-observe/eval-F167-2026-07-09:no-finding +- metric:c2.verdict_without_pass_count +- metric:c2.checked +- metric:c2.void_hold_hint_emitted +- metric:c2.void_hold_checked +- metric:inline_action.checked +- metric:grounding.check_total +- metric:grounding.mismatch_sample_count +- metadata:traceStoreStats/spanCount=321/counterWindowHours=18.80 + +Counterarguments: +- The 7/9 C2 denominator is only 61 after a process restart, while 7/8 had 551 checks; 0/61 should not be interpreted as a statistically strong improvement over 14/551. +- The 7/3-7/8 sequence rose monotonically from 1.21% to 2.54%, so one low-denominator clean window does not erase the drift hypothesis. +- No void_hold sample rows exist in 7/9 because no void_hold events fired; the persistent sample coverage question is therefore not resolved by this packet. +- Grounding shadow telemetry reported zero checks and zero samples; that is healthy for mismatch count, but does not prove the grounding path was exercised by stateful tool calls in this window. diff --git a/docs/harness-feedback/verdicts/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only.md b/docs/harness-feedback/verdicts/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only.md new file mode 100644 index 0000000000..cdbc34bd39 --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only.md @@ -0,0 +1,31 @@ +--- +feature_ids: [F245] +topics: [harness-eval, eval-friction, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:friction +packet_id: 2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only +source_snapshot: "snapshot:bundle/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/snapshot" +--- + +# Live Verdict — 2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only + +- Verdict: `keep_observe` +- Phenomenon: The every-3d friction window from 2026-07-06 03:00 UTC to 2026-07-09 03:00 UTC surfaced one new high-severity actionable singleton (`a2a_timeout: codex`) from the `Coactive 架构` thread, while the other four top clusters were recurring `eval:a2a` reference-only counters (`c2.void_hold_hint_emitted` plus three `inline_action.*` signals). The rollup remained degraded and the timeout had no second-channel echo, so the new signal looks like an isolated interruption rather than a stable cross-channel friction pattern. +- Harness: F245/friction-rollup (friction rollup (Top-N + sensorForm)) +- Root cause: Most likely `environment_drift`: the only actionable cluster is a confirmed `a2a_timeout: codex` driven by Codex CLI/backend disconnects in the `Coactive 架构` thread, while the remaining top clusters are recurring `eval:a2a` reference-only counters rather than a fresh friction-harness defect. Confidence stays low because this is a singleton and the rollup remained degraded (rule-only clustering). (confidence low) +- Owner ask: Keep the every-3d friction rollup running and escalate only if `a2a_timeout: codex` recurs, picks up a second channel, or the recurring `eval:a2a` reference-only clusters turn into a new source-domain verdict. +- Re-eval: next eval at 2026-07-12T03:00:00.000Z + +Evidence: +- snapshot:bundle/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/snapshot +- attribution:bundle/2026-07-09-eval-friction-timeout-singleton-with-recurring-reference-only/FR-2026-07-09-d981127d413a +- metric:friction-rollup.cluster_count +- metric:friction-rollup.top_cluster_count +- metric:friction-rollup.cluster_d981127d413a +- metric:friction-rollup.cluster_23fba4045727 + +Counterarguments: +- A single high-severity timeout that lasted more than twelve minutes may still justify immediate owner investigation instead of another observation cycle. +- The recurring reference-only `eval:a2a` counters may already be persistent enough that friction should escalate them again rather than only cite them. +- Because the current rollup was degraded, the apparent singleton may reflect under-clustering rather than a genuinely isolated event. \ No newline at end of file diff --git a/docs/harness-feedback/verdicts/2026-07-11-eval-a2a-low-traffic-clean-keep-observe.md b/docs/harness-feedback/verdicts/2026-07-11-eval-a2a-low-traffic-clean-keep-observe.md new file mode 100644 index 0000000000..a8b6e2305d --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-11-eval-a2a-low-traffic-clean-keep-observe.md @@ -0,0 +1,39 @@ +--- +feature_ids: [F192, F167] +topics: [harness-eval, eval-a2a, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:a2a +packet_id: 2026-07-11-eval-a2a-low-traffic-clean-keep-observe +source_snapshot: "snapshot:bundle/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/snapshot" +--- + +# Live Verdict — 2026-07-11-eval-a2a-low-traffic-clean-keep-observe + +- Verdict: `keep_observe` +- Phenomenon: The 2026-07-11 eval:a2a window is clean but low-traffic: C2 forced-pass and void-hold counters are 0/16, C1 zombie/cancel counters are 0, and grounding mismatch_sample_count is 0 across two stored grounding samples. The domain registry still has legacyScheduledTaskIds=[], so this is the ordinary daily trigger rather than a duplicate legacy scheduled task. +- Harness: F167/C2 (A2A Chain Quality runtime harness: exit-check / void-hold / grounding shadow telemetry) +- Owner ask: No code action from this packet. Continue ordinary eval:a2a monitoring; promote a separate F167 build/fix ask only if a higher-denominator window repeats void_hold drift above the 5% floor, verdict_without_pass returns, grounding mismatch_sample_count becomes positive with a recurring pattern, or sample coverage remains poor on recurring emissions. +- Re-eval: Keep daily eval active. Treat the regression as still watched until a high-denominator window remains below the 5% floor for C2 void_hold/verdict_without_pass and grounding mismatch_sample_count stays zero. at 2026-07-12T03:00:00.000Z + +Evidence: +- snapshot:bundle/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/snapshot +- attribution:bundle/2026-07-11-eval-a2a-low-traffic-clean-keep-observe/eval-F167-2026-07-11:no-finding +- metric:c2.verdict_without_pass_count +- metric:c2.checked +- metric:c2.void_hold_hint_emitted +- metric:c2.void_hold_checked +- metric:c1.hold_zombie_count +- metric:c1.hold_cancel_count +- metric:grounding.check_total +- metric:grounding.verdict_total +- metric:grounding.mismatch_sample_count +- metric:legacyScheduledTaskIds +- metadata:traceStoreStats/spanCount=81/counterWindowHours=13.00 +- metadata:groundingSamples/stored=2/mismatch=0 +- metadata:legacyScheduledTaskIds=0/legacyCleanup=disabled + +Counterarguments: +- 0/16 is clean but statistically weak; it does not erase the 7/3-7/8 monotonic void_hold drift noted in prior thread context. +- The latest committed baseline is 7/9 because 7/10 was interrupted, so this packet compares against the last durable artifact rather than a complete previous-day verdict. +- Grounding Phase O is healthy on mismatch count but under-exercised: one check counter and two insufficient stored samples are not enough to justify fail-closed escalation or closure. diff --git a/docs/harness-feedback/verdicts/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe.md b/docs/harness-feedback/verdicts/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe.md new file mode 100644 index 0000000000..3b0237c9e7 --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe.md @@ -0,0 +1,39 @@ +--- +feature_ids: [F192, F167] +topics: [harness-eval, eval-a2a, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:a2a +packet_id: 2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe +source_snapshot: "snapshot:bundle/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/snapshot" +--- + +# Live Verdict — 2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe + +- Verdict: `keep_observe` +- Phenomenon: The 2026-07-12 eval:a2a window is clean with a recovered C2 denominator: forced-pass is 0/108, void-hold is 0/109, C1 zombie/cancel counters are 0, and grounding mismatch_sample_count is 0 across four stored grounding samples. The domain registry still has legacyScheduledTaskIds=[], so this daily run is not a duplicate legacy trigger. +- Harness: F167/C2 (A2A Chain Quality runtime harness: exit-check / void-hold / grounding shadow telemetry) +- Owner ask: No code action from this packet. Continue ordinary eval:a2a monitoring; promote a separate F167 build/fix ask only if a high-denominator window repeats void_hold drift above the 5% floor, verdict_without_pass returns, grounding mismatch_sample_count becomes positive with a recurring pattern, or sample coverage remains poor on recurring emissions. +- Re-eval: Keep daily eval active. Treat the regression as ordinary monitored until another high-denominator clean window keeps C2 void_hold/verdict_without_pass below the 5% floor and grounding mismatch_sample_count stays zero. at 2026-07-13T03:00:00.000Z + +Evidence: +- snapshot:bundle/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/snapshot +- attribution:bundle/2026-07-12-eval-a2a-denominator-recovered-clean-keep-observe/eval-F167-2026-07-12:no-finding +- metric:c2.verdict_without_pass_count +- metric:c2.checked +- metric:c2.void_hold_hint_emitted +- metric:c2.void_hold_checked +- metric:c1.hold_zombie_count +- metric:c1.hold_cancel_count +- metric:grounding.check_total +- metric:grounding.verdict_total +- metric:grounding.mismatch_sample_count +- metric:legacyScheduledTaskIds +- metadata:traceStoreStats/spanCount=534/counterWindowHours=12.13 +- metadata:groundingSamples/stored=4/mismatch=0 +- metadata:legacyScheduledTaskIds=0/legacyCleanup=disabled + +Counterarguments: +- The day-over-day friction counts are flat at zero, so the improvement is mainly denominator/reliability rather than a lower count. +- The 7/11 durable packet is not present in this develop checkout, but GitHub shows PR #68 merged to main; this packet uses that as the prior durable thread baseline while publishing against origin/main. +- Grounding Phase O has no mismatches, but four insufficient samples indicate under-exercised resolver coverage rather than a reason to fail closed. diff --git a/docs/harness-feedback/verdicts/2026-07-12-eval-anchor-first-c1-keep-observe.md b/docs/harness-feedback/verdicts/2026-07-12-eval-anchor-first-c1-keep-observe.md new file mode 100644 index 0000000000..93d837ce2c --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-12-eval-anchor-first-c1-keep-observe.md @@ -0,0 +1,44 @@ +--- +feature_ids: [F192, F236] +topics: [harness-eval, eval-anchor-first, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:anchor-first +packet_id: 2026-07-12-eval-anchor-first-c1-keep-observe +source_snapshot: "snapshot:bundle/2026-07-12-eval-anchor-first-c1-keep-observe/snapshot" +--- + +# Live Verdict — 2026-07-12-eval-anchor-first-c1-keep-observe + +- Verdict: `keep_observe` +- Phenomenon: The latest 24h anchor-first window appears low-volume and concentrated in thread-context preview traffic, while independent blindness evidence is absent because eval:task-outcome has not yet produced published verdict trends. Current evidence is enough to watch adoption and open-rate detail, but not enough to justify sunset or a corrective fix. +- Harness: F236/anchor-telemetry-rollup (anchor-first preview/drill open-rate rollup) +- Owner ask: Keep F236 in observe mode, gather another full 24h anchor-telemetry window, and wait for eval:task-outcome trend evidence before considering fix or sunset. +- Re-eval: Re-evaluate when the next 24h rollup either shows a tool with enough preview volume to assess sunset signals confidently or eval:task-outcome publishes trend evidence that can confirm or reject blindness. at 2026-07-19T03:15:21.102Z + +Sunset Signal Assessment: +- thread-context: HEALTHY (openRate=0.9%, netBenefit=279197) + +Open-Rate Detail: +- thread-context: 0.9% open rate (1/110 items), charsSaved=280650, drillChars=1453, netBenefit=279197 +- Orphan drills: 0 + +Adoption Detail: +- explicitAnchorCalls=2; explicitFullCalls=3; uniqueCatsExplicitAnchor=1 +- defaultAnchorCalls=14; defaultFullCalls=0 +- legacyEquivalentAnchorCalls=0; legacyEquivalentFullCalls=1 +- unknownModeCalls=0 + +Evidence: +- snapshot:bundle/2026-07-12-eval-anchor-first-c1-keep-observe/snapshot +- attribution:bundle/2026-07-12-eval-anchor-first-c1-keep-observe/AF-2026-07-12-thread-context +- metric:prometheus:cat_cafe_anchor_returned_count_total{anchor_tool="thread-context"} +- metric:prometheus:cat_cafe_anchor_full_drill_count_total{anchor_tool="get-message"} +- metric:sqlite:task_outcome_episodes.with_verdict=0 +- data/transcripts/threads/thread_eval_anchor_first/gpt52/sessions/bb6104ac-0a88-4f75-81cd-b14e856c39db/events.live.jsonl +- task-outcome-episodes.sqlite + +Counterarguments: +- The live rollup could still show a high open-rate or negative net benefit on a low-visibility tool even though Track-1 counters look healthy at the aggregate level. +- Because task-outcome evidence is currently missing, this verdict may be underreacting to a real but unmeasured blindness effect. +- If the current process lifetime is materially shorter than 24h, the window may under-sample adoption and make the rollout look healthier than it is. diff --git a/docs/harness-feedback/verdicts/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline.md b/docs/harness-feedback/verdicts/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline.md new file mode 100644 index 0000000000..4df141e012 --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline.md @@ -0,0 +1,31 @@ +--- +feature_ids: [F245] +topics: [harness-eval, eval-friction, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:friction +packet_id: 2026-07-12-eval-friction-singleton-user-feedback-improved-baseline +source_snapshot: "snapshot:bundle/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/snapshot" +--- + +# Live Verdict — 2026-07-12-eval-friction-singleton-user-feedback-improved-baseline + +- Verdict: `keep_observe` +- Phenomenon: The every-3d friction window from 2026-07-09 03:00 UTC to 2026-07-12 03:00 UTC collapsed to one medium-severity actionable singleton, `text_frustration: 错了 什么情况`, with no reference-only eval-domain clusters and no long-tail spillover. Compared with the previous 72h window's 9 signals and 5 clusters, overall friction volume clearly improved even though one user-feedback incident remained. +- Harness: F245/friction-rollup (friction rollup (Top-N + sensorForm)) +- Root cause: Most likely a transient `execution_gap`: the only surviving cluster came from a thread where a cat asserted the wrong API/root-cause diagnosis before checking the active runtime path, and the user explicitly pushed back with `错了 / 什么情况`. Confidence stays low because the signal is a singleton, the rollup is still degraded, and no second channel or recurrence confirmed a stable failure mode. (confidence low) +- Owner ask: Keep the every-3d friction rollup running and escalate only if this user-feedback pattern recurs, gains a second channel, or a fresh reference-only eval-domain cluster reappears in the next window. +- Re-eval: next eval at 2026-07-15T03:00:00.000Z + +Evidence: +- snapshot:bundle/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/snapshot +- attribution:bundle/2026-07-12-eval-friction-singleton-user-feedback-improved-baseline/FR-2026-07-12-4e7626161f05 +- metric:friction-rollup.cluster_count +- metric:friction-rollup.top_cluster_count +- metric:friction-rollup.tail_signal_count +- metric:friction-rollup.cluster_4e7626161f05 + +Counterarguments: +- Even as a singleton, direct user pushback that a diagnosis was wrong can justify immediate owner attention instead of another observation cycle. +- Because the current rollup remained degraded, related signals may have been missed and made a broader pattern look smaller than it is. +- The disappearance of reference-only clusters may reflect quiet source-domain activity rather than a real improvement in friction quality. \ No newline at end of file diff --git a/docs/harness-feedback/verdicts/2026-07-12-eval-qc-zero-baseline-w28.md b/docs/harness-feedback/verdicts/2026-07-12-eval-qc-zero-baseline-w28.md new file mode 100644 index 0000000000..e6010cc717 --- /dev/null +++ b/docs/harness-feedback/verdicts/2026-07-12-eval-qc-zero-baseline-w28.md @@ -0,0 +1,36 @@ +--- +feature_ids: [F253] +topics: [harness-eval, eval-qc, live-verdict] +doc_kind: harness-feedback +feedback_type: live-verdict +domain_id: eval:qc +packet_id: 2026-07-12-eval-qc-zero-baseline-w28 +source_snapshot: "snapshot:bundle/2026-07-12-eval-qc-zero-baseline-w28/snapshot" +--- + +# eval:qc Verdict — 2026-07-12-eval-qc-zero-baseline-w28 + +- Verdict: `keep_observe` +- Phenomenon: QC pipeline metrics remain in zero-baseline state — no review telemetry events collected during the Jul 5–12 window. Phase C bootstrap: the qc-metrics-provider returns zeroes for all 4 metrics (finding yield, false positive rate, reviewer delta, post-merge bug rate) because no live data source is wired yet. +- Harness: F253/qc-pipeline (QC Review Loop) +- Owner ask: No action required. Continue observing until a future phase wires live review telemetry events into the QC metrics provider. +- Re-eval: next eval at 2026-07-19T03:00:00.000Z + +Evidence: +- snapshot:bundle/2026-07-12-eval-qc-zero-baseline-w28/snapshot +- attribution:bundle/2026-07-12-eval-qc-zero-baseline-w28/qc-snapshot-2026-07-12-eval-qc-zero-baseline-w28:no-finding + +**Window**: 7 days | **PRs analyzed**: 0 + +## Metrics + +| Metric | Value | +|--------|-------| +| Finding Yield (avg/review) | 0 | +| False Positive Rate | 0 | +| Reviewer Delta | 0 | +| Post-Merge Bug Rate | 0 | + +## Notes + +No PR data available in this window. Zero-baseline snapshot (Phase C bootstrap — live data sources not yet wired). diff --git a/packages/api/config/public-test-exclusions.json b/packages/api/config/public-test-exclusions.json index 130639db51..e19a2b5a0c 100644 --- a/packages/api/config/public-test-exclusions.json +++ b/packages/api/config/public-test-exclusions.json @@ -341,7 +341,7 @@ "reason": "F236 cc anchor hook coverage imports the source-only root .claude hook implementation that is not part of the public export.", "owner": "@zts212653", "introducedBy": "68dd499d9", - "expiresOn": "2026-07-07" + "expiresOn": "2026-07-31" }, { "id": "github-schedule-factories", diff --git a/packages/api/src/config/context-window-sizes.ts b/packages/api/src/config/context-window-sizes.ts index 1299503e3f..5b32947d82 100644 --- a/packages/api/src/config/context-window-sizes.ts +++ b/packages/api/src/config/context-window-sizes.ts @@ -13,6 +13,11 @@ export const CONTEXT_WINDOW_SIZES: Record = { 'claude-opus-4-6': 200_000, 'claude-sonnet-4-5': 200_000, 'claude-haiku-4-5': 200_000, + // Fable 5: native 1M context — the maximum is also the default (no [1m] + // suffix needed). Also listed in KNOWN_MIN_CONTEXT_WINDOWS below because + // stale CLIs (≤2.1.177) mis-REPORT it as 200K, which this table alone + // cannot fix (CLI report outranks the fallback table). + 'claude-fable-5': 1_000_000, // Codex/GPT 'gpt-5.3': 128_000, 'gpt-5.2': 128_000, @@ -49,25 +54,78 @@ export const CONTEXT_WINDOW_SIZES: Record = { */ export const OPENCODE_DEFAULT_CONTEXT_WINDOW = 128_000; -export function getContextWindowFallback(model: string): number | undefined { - // Normalize provider-prefixed model IDs before lookup. The account routing - // path in invoke-single-cat sets `callbackEnv.CAT_CAFE_ANTHROPIC_MODEL_OVERRIDE` - // to a `safeProvider/model` form (see L1459 `safeProvider/safeModel`), and - // OpenCodeAgentService propagates that prefixed string as `metadata.model`. - // Without normalization, lookups like `anthropic/claude-opus-4-6` or - // `openai-compat/gpt-5.3` would miss the table entirely → no windowSize → - // F24 context_health silently skipped → opencode handoff (clowder#915) - // bypassed in production. (clowder#915 R2 cloud P1) - // - // Use lastIndexOf to handle multi-segment prefixes like `openai-compat/x/y` - // (defensive — current code emits at most one slash, but the cost is the - // same and we don't want to be the next migration's footgun). +// Normalize provider-prefixed model IDs before lookup. The account routing +// path in invoke-single-cat sets `callbackEnv.CAT_CAFE_ANTHROPIC_MODEL_OVERRIDE` +// to a `safeProvider/model` form (see L1459 `safeProvider/safeModel`), and +// OpenCodeAgentService propagates that prefixed string as `metadata.model`. +// Without normalization, lookups like `anthropic/claude-opus-4-6` or +// `openai-compat/gpt-5.3` would miss the table entirely → no windowSize → +// F24 context_health silently skipped → opencode handoff (clowder#915) +// bypassed in production. (clowder#915 R2 cloud P1) +// +// Use lastIndexOf to handle multi-segment prefixes like `openai-compat/x/y` +// (defensive — current code emits at most one slash, but the cost is the +// same and we don't want to be the next migration's footgun). +function stripProviderPrefix(model: string): string { const slashAt = model.lastIndexOf('/'); - const bare = slashAt >= 0 ? model.slice(slashAt + 1) : model; - if (CONTEXT_WINDOW_SIZES[bare]) return CONTEXT_WINDOW_SIZES[bare]; - // Try prefix match (e.g. 'claude-opus-4-6-20260101' matches 'claude-opus-4-6') - for (const [key, value] of Object.entries(CONTEXT_WINDOW_SIZES)) { + return slashAt >= 0 ? model.slice(slashAt + 1) : model; +} + +function lookupWithPrefixMatch(table: Record, bare: string): number | undefined { + if (table[bare]) return table[bare]; + // Prefix match (e.g. 'claude-opus-4-6-20260101' matches 'claude-opus-4-6') + for (const [key, value] of Object.entries(table)) { if (bare.startsWith(key)) return value; } return undefined; } + +export function getContextWindowFallback(model: string): number | undefined { + return lookupWithPrefixMatch(CONTEXT_WINDOW_SIZES, stripProviderPrefix(model)); +} + +/** + * Known-minimum context windows — authoritative floors used to correct + * STALE CLI-reported window sizes, applied as `max(reported, floor)`. + * + * Why this exists (F24 follow-up): the Claude CLI reports + * `modelUsage[*].contextWindow` and invoke-single-cat trusts that report + * FIRST — so a stale CLI ships a stale window and the fallback table + * above can never correct it. Proven in production: CLI 2.1.177 reported + * 200_000 for `claude-fable-5` (native 1M) while the very same turn + * consumed 303K input tokens without error; auto-seal fired at + * "fillRatio 1.0" with 80% of the real window unused (sessions + * 59a48070 / 6b8d4b5f, thread_mraghcf19yl6ukzu, 2026-07-08). + * + * Rules: + * - `[1m]` suffix: Claude Code's own "run at 1M context" directive — if + * the CLI accepted the model string, 1M IS the session window. + * - Table entries: ONLY models whose window we know from official specs + * with certainty. An over-estimate defeats auto-seal (the session + * would drift into CLI auto-compact instead of sealing with a clean + * handoff), so keep this list conservative. + * - `max()` semantics: never shrinks a CLI report. Once the CLI catches + * up (2.1.204+ presumably reports 1M), the floor becomes a no-op. + */ +const KNOWN_MIN_CONTEXT_WINDOWS: Record = { + 'claude-fable-5': 1_000_000, +}; + +export function getKnownMinContextWindow(model: string): number | undefined { + const bare = stripProviderPrefix(model); + if (bare.endsWith('[1m]')) return 1_000_000; + return lookupWithPrefixMatch(KNOWN_MIN_CONTEXT_WINDOWS, bare); +} + +/** + * Single resolution point for "how big is this session's context window": + * CLI-reported value → fallback table, then raised to any known + * authoritative floor. Callers needing a provider-specific last resort + * (e.g. opencode's 128K default) apply it AFTER this returns undefined. + */ +export function resolveContextWindow(reported: number | undefined, model: string): number | undefined { + const base = reported ?? getContextWindowFallback(model); + const floor = getKnownMinContextWindow(model); + if (base != null && floor != null) return Math.max(base, floor); + return base ?? floor; +} 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 c7c0ab2e51..a31fc4d2b1 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 @@ -34,10 +34,7 @@ import { resolveBoundAccountRefForCat } from '../../../../../config/cat-account- import { isSessionChainEnabled } from '../../../../../config/cat-config-loader.js'; import { buildCatGitIdentityEnv } from '../../../../../config/cat-git-identity.js'; import { getCatModel } from '../../../../../config/cat-models.js'; -import { - getContextWindowFallback, - OPENCODE_DEFAULT_CONTEXT_WINDOW, -} from '../../../../../config/context-window-sizes.js'; +import { OPENCODE_DEFAULT_CONTEXT_WINDOW, resolveContextWindow } from '../../../../../config/context-window-sizes.js'; import { getSessionStrategy, shouldTakeAction } from '../../../../../config/session-strategy.js'; import { assertSafeTestConfigRoot } from '../../../../../config/test-config-write-guard.js'; import { capturePromptIfEnabled } from '../../../../../infrastructure/debug/prompt-capture-bridge.js'; @@ -2366,9 +2363,13 @@ export async function* invokeSingleCat(deps: InvocationDeps, params: InvocationP // actually targets). Crucially this is LAST so known opencode // models like the default claude-opus-4-6 get their precise 200k // from the table, NOT the 128k conservative default. + // F24 known-min floor: tiers 1-2 are additionally raised to + // max(value, known floor) inside resolveContextWindow — a stale + // CLI (≤2.1.177) reports 200K for claude-fable-5 (native 1M), + // and tier 1 outranking tier 2 meant the wrong value always won: + // sessions sealed budget_exhausted with 80% of the window unused. const windowSize = - msg.metadata.usage.contextWindowSize ?? - getContextWindowFallback(msg.metadata.model ?? '') ?? + resolveContextWindow(msg.metadata.usage.contextWindowSize, msg.metadata.model ?? '') ?? (msg.metadata.provider === 'opencode' ? OPENCODE_DEFAULT_CONTEXT_WINDOW : undefined); const usedFrom = msg.metadata.usage.lastTurnInputTokens != null 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 407af55eaa..cc85b3f5ad 100644 --- a/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts +++ b/packages/api/src/domains/cats/services/agents/providers/OpenCodeAgentService.ts @@ -20,18 +20,22 @@ 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, - isCliPlainTextResult, - isCliTimeout, - isLivenessWarning, - spawnCli, -} from '../../../../../utils/cli-spawn.js'; +import { isCliError, isCliTimeout, isLivenessWarning, spawnCli } from '../../../../../utils/cli-spawn.js'; import type { SpawnFn } from '../../../../../utils/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'; import { sanitizeRawEvent } from '../providers/codex-audit-hooks.js'; +import { + cacheOpenCodeAutoApproveProbe, + getCliFlagName, + OPENCODE_AUTO_APPROVE_FLAG, + type OpenCodeAutoApproveProbeFn, + type OpenCodeAutoApproveProbeResult, + parseOpenCodeCliConfigArgs, + probeOpenCodeAutoApproveSupport, + userControlsOpenCodeAutoApprove, +} from './opencode-auto-approval.js'; import { transformOpenCodeEvent } from './opencode-event-transform.js'; const log = createModuleLogger('opencode-agent'); @@ -57,27 +61,6 @@ interface OpenCodeAgentServiceOptions { const OPENCODE_API_KEY_ENV = 'OPENCODE_API_KEY'; const ANTHROPIC_API_KEY_ENV = 'ANTHROPIC_API_KEY'; const ANTHROPIC_BASE_URL_ENV = 'ANTHROPIC_BASE_URL'; -const OPENCODE_AUTO_APPROVE_FLAG = '--auto'; -const OPENCODE_AUTO_APPROVE_FLAG_ALIASES = new Set([ - OPENCODE_AUTO_APPROVE_FLAG, - '--yolo', - '--dangerously-skip-permissions', - '--no-auto', - '--no-yolo', - '--no-dangerously-skip-permissions', -]); -const OPENCODE_AUTO_APPROVE_MIN_VERSION = '1.17.12'; -const OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS = 10_000; -const OPENCODE_AUTO_APPROVE_UNSUPPORTED_MESSAGE = `OpenCode 版本过低,不支持 --auto 自动审批;请升级 opencode-ai 到 >= ${OPENCODE_AUTO_APPROVE_MIN_VERSION} 后重试。`; -const OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE = `无法确认 OpenCode 是否支持 --auto 自动审批;请升级 opencode-ai 到 >= ${OPENCODE_AUTO_APPROVE_MIN_VERSION} 后重试。`; - -type OpenCodeAutoApproveProbeResult = { supported: true } | { supported: false; message: string }; -type OpenCodeAutoApproveProbeFn = (options: { - command: string; - cwd?: string; - env?: Record; -}) => Promise; - // Process-wide cache: --auto support is a property of the installed opencode binary. // Restart the API process after upgrading opencode so this capability is re-probed. let sharedOpenCodeAutoApproveProbe: Promise | undefined; @@ -105,49 +88,6 @@ function summarizeDebugSecret(value: string | null | undefined): string { return `${value.slice(0, 6)}***`; } -function getCliFlagName(part: string): string | null { - if (!part.startsWith('-')) return null; - const equalsIndex = part.indexOf('='); - return equalsIndex > 0 ? part.slice(0, equalsIndex) : part; -} - -async function probeOpenCodeAutoApproveSupport( - command: string, - cwd?: string, - env?: Record, -): Promise { - let helpText = ''; - try { - for await (const event of spawnCli({ - command, - args: ['run', '--help'], - outputMode: 'plainText', - ...(cwd ? { cwd } : {}), - ...(env ? { env } : {}), - timeoutMs: OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS, - })) { - if (isCliPlainTextResult(event)) { - helpText = `${event.stdout}\n${event.stderr}`; - continue; - } - if (isCliTimeout(event)) { - log.warn({ command, timeoutMs: OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS }, 'OpenCode --auto probe timed out'); - return { supported: false, message: OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE }; - } - if (isCliError(event)) { - log.warn({ command, exitCode: event.exitCode, signal: event.signal }, 'OpenCode --auto probe failed'); - return { supported: false, message: OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE }; - } - } - } catch (err) { - log.warn({ command, err }, 'OpenCode --auto probe threw'); - return { supported: false, message: OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE }; - } - - if (helpText.includes('--auto')) return { supported: true }; - return { supported: false, message: OPENCODE_AUTO_APPROVE_UNSUPPORTED_MESSAGE }; -} - export function summarizeOpenCodeEnvForDebug(env: Record | undefined): OpenCodeEnvDebugSummary { const profileMode = env?.CAT_CAFE_ANTHROPIC_PROFILE_MODE ?? '(unset)'; const hasRuntimeConfig = Boolean(env?.OPENCODE_CONFIG); @@ -243,8 +183,19 @@ export class OpenCodeAgentService implements L0InjectableAgentService { return; } - await this.ensureAutoApproveSupported(opencodeCommand, cwd, childEnv); - const args = this.buildArgs(prompt, options?.sessionId, effectiveModel, options?.cliConfigArgs); + const defaultAutoApproveFlag = await this.resolveDefaultAutoApproveFlag( + opencodeCommand, + cwd, + childEnv, + options?.cliConfigArgs, + ); + const args = this.buildArgs( + prompt, + options?.sessionId, + effectiveModel, + options?.cliConfigArgs, + defaultAutoApproveFlag, + ); log.debug( { @@ -479,7 +430,13 @@ export class OpenCodeAgentService implements L0InjectableAgentService { } } - private buildArgs(prompt: string, sessionId?: string, model?: string, cliConfigArgs?: readonly string[]): string[] { + private buildArgs( + prompt: string, + sessionId?: string, + model?: string, + cliConfigArgs?: readonly string[], + defaultAutoApproveFlag?: string, + ): string[] { const args = ['run']; // Session resume @@ -495,18 +452,16 @@ export class OpenCodeAgentService implements L0InjectableAgentService { // JSON event stream output args.push('--format', 'json'); - // Headless OpenCode has no human approval bridge. --auto is the public - // opencode flag since 1.17.12; ensureAutoApproveSupported gates older CLIs. - args.push(OPENCODE_AUTO_APPROVE_FLAG); + // Headless OpenCode has no human approval bridge. Use the best approval + // flag advertised by this installed CLI; older compatible builds may have + // no supported flag, in which case we preserve the pre-#1065 behavior. + if (defaultAutoApproveFlag) args.push(defaultAutoApproveFlag); // User-defined CLI args from the member editor (#567). // User args win when they overlap with system-injected flags. - const userParts: string[] = []; - for (const arg of cliConfigArgs ?? []) { - userParts.push(...arg.trim().split(/\s+/)); - } + const userParts = parseOpenCodeCliConfigArgs(cliConfigArgs); const userFlags = new Set(userParts.map(getCliFlagName).filter((flag): flag is string => flag !== null)); - const userControlsAutoApprove = Array.from(OPENCODE_AUTO_APPROVE_FLAG_ALIASES).some((flag) => userFlags.has(flag)); + const userControlsAutoApprove = userControlsOpenCodeAutoApprove(userFlags); const deduped: string[] = []; for (let i = 0; i < args.length; i++) { const flagName = getCliFlagName(args[i]); @@ -524,15 +479,24 @@ export class OpenCodeAgentService implements L0InjectableAgentService { return deduped; } - private async ensureAutoApproveSupported( + private async resolveDefaultAutoApproveFlag( command: string, cwd?: string, env?: Record, - ): Promise { + cliConfigArgs?: readonly string[], + ): Promise { + const userParts = parseOpenCodeCliConfigArgs(cliConfigArgs); + const userFlags = new Set(userParts.map(getCliFlagName).filter((flag): flag is string => flag !== null)); + if (userControlsOpenCodeAutoApprove(userFlags)) return undefined; + const result = await this.getAutoApproveProbe(command, cwd, env); - // Reject before launching the real headless run. invoke() surfaces this as - // error + done with upgrade guidance instead of continuing without approvals. - if (!result.supported) throw new Error(result.message); + if (result.warning) { + log.warn( + { catId: this.catId, command, warning: result.warning }, + 'OpenCode auto-approval flag unavailable; continuing without default flag', + ); + } + return result.approvalFlag; } private getAutoApproveProbe( @@ -541,15 +505,25 @@ export class OpenCodeAgentService implements L0InjectableAgentService { env?: Record, ): Promise { if (this.autoApproveProbeFn) { - this.autoApproveProbe ??= this.autoApproveProbeFn({ command, ...(cwd ? { cwd } : {}), ...(env ? { env } : {}) }); + this.autoApproveProbe ??= cacheOpenCodeAutoApproveProbe( + this.autoApproveProbeFn({ command, ...(cwd ? { cwd } : {}), ...(env ? { env } : {}) }), + (promise) => { + if (this.autoApproveProbe === promise) this.autoApproveProbe = undefined; + }, + ); return this.autoApproveProbe; } // Unit tests inject spawnFn to own the primary CLI process lifecycle. Do not // consume that mock for the preflight probe unless the test provides an // explicit autoApproveProbeFn. - if (this.spawnFn) return Promise.resolve({ supported: true }); - - sharedOpenCodeAutoApproveProbe ??= probeOpenCodeAutoApproveSupport(command, cwd, env); + if (this.spawnFn) return Promise.resolve({ approvalFlag: OPENCODE_AUTO_APPROVE_FLAG }); + + sharedOpenCodeAutoApproveProbe ??= cacheOpenCodeAutoApproveProbe( + probeOpenCodeAutoApproveSupport(command, cwd, env), + (promise) => { + if (sharedOpenCodeAutoApproveProbe === promise) sharedOpenCodeAutoApproveProbe = undefined; + }, + ); return sharedOpenCodeAutoApproveProbe; } diff --git a/packages/api/src/domains/cats/services/agents/providers/opencode-auto-approval.ts b/packages/api/src/domains/cats/services/agents/providers/opencode-auto-approval.ts new file mode 100644 index 0000000000..0aea7351a8 --- /dev/null +++ b/packages/api/src/domains/cats/services/agents/providers/opencode-auto-approval.ts @@ -0,0 +1,139 @@ +import { createModuleLogger } from '../../../../../infrastructure/logger.js'; +import { isCliError, isCliPlainTextResult, isCliTimeout, spawnCli } from '../../../../../utils/cli-spawn.js'; + +const log = createModuleLogger('opencode-agent'); + +export const OPENCODE_AUTO_APPROVE_FLAG = '--auto'; +const OPENCODE_DEFAULT_AUTO_APPROVE_FLAGS = [ + OPENCODE_AUTO_APPROVE_FLAG, + '--dangerously-skip-permissions', + '--yolo', +] as const; +const OPENCODE_AUTO_APPROVE_FLAG_ALIASES = new Set([ + ...OPENCODE_DEFAULT_AUTO_APPROVE_FLAGS, + '--no-auto', + '--no-yolo', + '--no-dangerously-skip-permissions', +]); +const OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS = 10_000; +const OPENCODE_AUTO_APPROVE_UNAVAILABLE_MESSAGE = + 'OpenCode run --help did not advertise a supported auto-approval flag; continuing without default approval flag injection.'; +const OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE = + 'Unable to confirm OpenCode auto-approval flag support; continuing without default approval flag injection.'; + +export type OpenCodeAutoApproveProbeResult = { approvalFlag?: string; warning?: string; cacheable?: boolean }; +export type OpenCodeAutoApproveProbeFn = (options: { + command: string; + cwd?: string; + env?: Record; +}) => Promise; +type OpenCodeHelpProbeResult = + | { status: 'ok'; helpText: string } + | { status: 'unsupported' } + | { status: 'transient-failure' }; + +export function getCliFlagName(part: string): string | null { + if (!part.startsWith('-')) return null; + const equalsIndex = part.indexOf('='); + return equalsIndex > 0 ? part.slice(0, equalsIndex) : part; +} + +export function parseOpenCodeCliConfigArgs(cliConfigArgs?: readonly string[]): string[] { + const userParts: string[] = []; + for (const arg of cliConfigArgs ?? []) { + userParts.push(...arg.trim().split(/\s+/)); + } + return userParts; +} + +export function userControlsOpenCodeAutoApprove(userFlags: ReadonlySet): boolean { + return Array.from(OPENCODE_AUTO_APPROVE_FLAG_ALIASES).some((flag) => userFlags.has(flag)); +} + +async function probeOpenCodeHelp( + command: string, + args: readonly string[], + cwd?: string, + env?: Record, + options?: { cliErrorAsUnsupported?: boolean }, +): Promise { + let helpText = ''; + try { + for await (const event of spawnCli({ + command, + args, + outputMode: 'plainText', + ...(cwd ? { cwd } : {}), + ...(env ? { env } : {}), + timeoutMs: OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS, + })) { + if (isCliPlainTextResult(event)) { + helpText = `${event.stdout}\n${event.stderr}`; + continue; + } + if (isCliTimeout(event)) { + log.warn({ command, timeoutMs: OPENCODE_AUTO_APPROVE_PROBE_TIMEOUT_MS }, 'OpenCode help probe timed out'); + return { status: 'transient-failure' }; + } + if (isCliError(event)) { + log.warn({ command, exitCode: event.exitCode, signal: event.signal }, 'OpenCode help probe failed'); + return options?.cliErrorAsUnsupported ? { status: 'unsupported' } : { status: 'transient-failure' }; + } + } + } catch (err) { + log.warn({ command, err }, 'OpenCode help probe threw'); + return { status: 'transient-failure' }; + } + + return { status: 'ok', helpText }; +} + +export async function probeOpenCodeAutoApproveSupport( + command: string, + cwd?: string, + env?: Record, +): Promise { + const visibleHelp = await probeOpenCodeHelp(command, ['run', '--help'], cwd, env); + if (visibleHelp.status !== 'ok') { + return { warning: OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE, cacheable: false }; + } + + for (const flag of OPENCODE_DEFAULT_AUTO_APPROVE_FLAGS) { + if (visibleHelp.helpText.includes(flag)) return { approvalFlag: flag }; + } + + for (const flag of OPENCODE_DEFAULT_AUTO_APPROVE_FLAGS.slice(1)) { + const hiddenHelp = await probeOpenCodeHelp(command, ['run', flag, '--help'], cwd, env, { + cliErrorAsUnsupported: true, + }); + if (hiddenHelp.status === 'ok') return { approvalFlag: flag }; + if (hiddenHelp.status === 'transient-failure') { + return { warning: OPENCODE_AUTO_APPROVE_PROBE_FAILED_MESSAGE, cacheable: false }; + } + } + + return { warning: OPENCODE_AUTO_APPROVE_UNAVAILABLE_MESSAGE, cacheable: true }; +} + +function isOpenCodeAutoApproveProbeCacheable(result: OpenCodeAutoApproveProbeResult): boolean { + if (result.cacheable !== undefined) return result.cacheable; + if (result.warning && !result.approvalFlag) return result.warning === OPENCODE_AUTO_APPROVE_UNAVAILABLE_MESSAGE; + return true; +} + +export function cacheOpenCodeAutoApproveProbe( + probe: Promise, + clearCache: (promise: Promise) => void, +): Promise { + const cachedProbe = probe.then( + (result) => { + if (!isOpenCodeAutoApproveProbeCacheable(result)) clearCache(cachedProbe); + return result; + }, + (err) => { + clearCache(cachedProbe); + throw err; + }, + ); + return cachedProbe; +} 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 c318201658..7e7cd960c2 100644 --- a/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts +++ b/packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts @@ -595,6 +595,8 @@ export interface AgentRouterOptions { freshnessReinvokeCheck?: import('../invocation/invoke-single-cat.js').InvocationDeps['freshnessReinvokeCheck']; /** F254 Phase C: Freshness state store for carrier tier persistence */ freshnessStateStore?: import('../../freshness/FreshnessInvocationStateStore.js').FreshnessInvocationStateStore; + /** F237 Phase 2 (AC-P2-8): Injection trace store for pipeline observability */ + injectionTraceStore?: import('../../../../prompt-hooks/InjectionTraceStore.js').InjectionTraceStore; } /** @@ -665,6 +667,8 @@ export class AgentRouter { private freshnessReinvokeCheck?: import('../invocation/invoke-single-cat.js').InvocationDeps['freshnessReinvokeCheck']; /** F254 Phase C */ private freshnessStateStore?: import('../../freshness/FreshnessInvocationStateStore.js').FreshnessInvocationStateStore; + /** F237 Phase 2 (AC-P2-8) */ + private injectionTraceStore?: import('../../../../prompt-hooks/InjectionTraceStore.js').InjectionTraceStore; private speechMentionRe: RegExp; /** @@ -774,6 +778,7 @@ export class AgentRouter { this.cloudInvokeBridge = options.cloudInvokeBridge; this.freshnessReinvokeCheck = options.freshnessReinvokeCheck; this.freshnessStateStore = options.freshnessStateStore; + this.injectionTraceStore = options.injectionTraceStore; } refreshFromRegistry(agentRegistry: AgentRegistry): void { @@ -1386,6 +1391,7 @@ export class AgentRouter { ...(this.frustrationIssueStore ? { frustrationIssueStore: this.frustrationIssueStore } : {}), ...(this.pendingRequestStore ? { pendingRequestStore: this.pendingRequestStore } : {}), ...(this.ballCustody ? { ballCustody: this.ballCustody } : {}), + ...(this.injectionTraceStore ? { injectionTraceStore: this.injectionTraceStore } : {}), }; } 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 e3142cc792..ca0b3926fb 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 @@ -72,6 +72,8 @@ export interface RouteStrategyDeps { worldStore?: import('../../../../world/interfaces.js').IWorldStore; /** F233 Phase B (B2): Ball-custody ingest — fire-and-forget 旁路写球权事件(append + appended-guard apply)。optional, fail-open */ ballCustody?: import('../../../../ball-custody/BallCustodyIngest.js').IBallCustodyIngest; + /** F237 Phase 2 (AC-P2-8): Injection trace store for pipeline observability. optional, fail-open */ + injectionTraceStore?: import('../../../../prompt-hooks/InjectionTraceStore.js').InjectionTraceStore; } /** Mutable context for tracking persistence failures across the generator boundary. diff --git a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts index 4ad65984a6..170dd9dd44 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-parallel.ts @@ -29,6 +29,7 @@ import { prepareGuideContext, } from '../../../../guides/GuideRoutingInterceptor.js'; import { triggerRecallCorrelation } from '../../../../memory/recall-correlation-hook.js'; +import { drainCapturedTraces } from '../../../../prompt-hooks/PipelinePromptBuilder.js'; import { getTraceStore } from '../../../../prompt-hooks/trace-bootstrap.js'; // F237: Injection trace (v0 — fire-and-forget observability) import { buildTraceDetail, buildTraceSummary, collectTrace } from '../../../../prompt-hooks/trace-collector.js'; @@ -244,6 +245,9 @@ export async function* routeParallel( const staticIdentity = hasNativeL0 ? buildStaticIdentityPackOnly(catId, { packBlocks }) : buildStaticIdentity(catId, { mcpAvailable, packBlocks }); + // F237: drain session trace IMMEDIATELY — before any await that could let + // another parallel cat overwrite the module-global capture buffer. + drainCapturedTraces(); // F041: inject HTTP callback only when MCP is NOT actually available (fallback) const mcpInstructions = needsMcpInjection(mcpAvailable, catConfig?.clientId) ? buildMcpCallbackInstructions({ @@ -314,6 +318,13 @@ export async function* routeParallel( ...guideContextForCat(guideCtx, catId, targetCatIds, threadId), ...conciergeContextForCat(conciergeCtx, catId as string), }); + // F237: drain turn trace IMMEDIATELY — same race-safety as session drain above. + drainCapturedTraces(); + + // F237 Phase 2: Pipeline trace capture drained above (lines 250, 322) to prevent + // stale module-global buffer in concurrent Promise.all execution. Persistence is + // handled by the v0 trace path below (after all route-level content is assembled). + const continuityCapsule = buildCapsuleFromRouteState({ threadId, catId: catId as string, @@ -393,6 +404,10 @@ export async function* routeParallel( log.warn({ err, threadId, catId }, '[F237] injection trace persist failed (fire-and-forget)'); }); } + // v0 collectTrace → buildStaticIdentity(annotateSegments: true) re-populates + // the module-global capturedSessionTrace without draining. Clear it so the next + // invocation (especially native-L0 pack-only) doesn't persist stale session traces. + if (deps.injectionTraceStore) drainCapturedTraces(); } catch { /* F237: trace collection must never break invocation */ } 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 6e3d4bb130..3491735145 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 @@ -75,6 +75,7 @@ import { prepareGuideContext, } from '../../../../guides/GuideRoutingInterceptor.js'; import { triggerRecallCorrelation } from '../../../../memory/recall-correlation-hook.js'; +import { drainCapturedTraces } from '../../../../prompt-hooks/PipelinePromptBuilder.js'; import { getTraceStore } from '../../../../prompt-hooks/trace-bootstrap.js'; // F237: Injection trace (v0 — fire-and-forget observability) import { buildTraceDetail, buildTraceSummary, collectTrace } from '../../../../prompt-hooks/trace-collector.js'; @@ -716,6 +717,9 @@ export async function* routeSerial( const staticIdentity = hasNativeL0 ? buildStaticIdentityPackOnly(catId, { packBlocks }) : buildStaticIdentity(catId, { mcpAvailable, packBlocks }); + // F237: drain session trace synchronously — before any await between + // buildStaticIdentity and buildInvocationContext (race-safety for parallel reuse). + drainCapturedTraces(); // L0-budget-defense PR-B-impl (ADR-038 件套 ④): staging is NOT prepended // to staticIdentity here. Cloud R2 P1 #2237 L1099: folding staging into // staticIdentity breaks ADR-038 "每轮注入生效" contract on resumed @@ -820,6 +824,8 @@ export async function* routeSerial( ...(worldContext ? { worldContext } : {}), ...conciergeContextForCat(conciergeCtx, catId as string), }); + // F237: drain turn trace synchronously — no yield between build and drain. + drainCapturedTraces(); const continuityCapsule = buildCapsuleFromRouteState({ threadId, catId: catId as string, @@ -834,6 +840,11 @@ export async function* routeSerial( maxA2ADepth: maxDepth, }); + // F237 Phase 2: Pipeline trace capture is drained above (lines 698, 804) + // to prevent stale module-global buffer. Persistence is handled by the v0 + // trace path below (after all route-level content is assembled), avoiding + // duplicate records. Phase 2 will take over persistence when migration completes. + // F24 Phase E: Bootstrap context for Session #2+ // #836: Reborn cats skip bootstrap — every invocation starts with zero prior context. // Uses store lookup (not thread field) — Redis memberSS:* fields aren't hydrated by get(). @@ -900,6 +911,10 @@ export async function* routeSerial( log.warn({ err, threadId, catId }, '[F237] injection trace persist failed (fire-and-forget)'); }); } + // v0 collectTrace → buildStaticIdentity(annotateSegments: true) re-populates + // the module-global capturedSessionTrace without draining. Clear it so the next + // invocation (especially native-L0 pack-only) doesn't persist stale session traces. + if (deps.injectionTraceStore) drainCapturedTraces(); } catch { /* F237: trace collection must never break invocation */ } diff --git a/packages/api/src/domains/cats/services/context/SystemPromptBuilder.ts b/packages/api/src/domains/cats/services/context/SystemPromptBuilder.ts index fa19c645f2..c4742f8bf3 100644 --- a/packages/api/src/domains/cats/services/context/SystemPromptBuilder.ts +++ b/packages/api/src/domains/cats/services/context/SystemPromptBuilder.ts @@ -21,8 +21,11 @@ import { findMonorepoRoot } from '../../../../utils/monorepo-root.js'; // F167 Phase F P1 (cloud Codex): roster model cell must resolve via getCatModel // (env CAT_{CATID}_MODEL → registry → defaults), not from static config.defaultModel, // otherwise env overrides cause exactly the handle/model drift Phase F is killing. -import { buildConciergePromptLines } from '../../../concierge/ConciergePromptSection.js'; -import { buildGuidePromptLines } from '../../../guides/GuidePromptSection.js'; +// F237 Phase 2 (AC-P2-6): pipeline-backed builders for delegation +import { + buildInvocationContextViaHookPipeline, + buildStaticIdentityViaHookPipeline, +} from '../../../prompt-hooks/PipelinePromptBuilder.js'; import type { BootcampStateV1, ThreadMentionRoutingFeedback, @@ -30,13 +33,7 @@ import type { ThreadRoutingPolicyV1, } from '../stores/ports/ThreadStore.js'; import { loadCompiledGovernanceL0, loadCompiledGovernanceL0Sync } from './governance-l0.js'; -import { - loadA2aBallCheck, - loadHandoffDecisionTree, - loadMcpToolsSection, - loadWorkflowTriggers, - renderSegment, -} from './prompt-template-loader.js'; +import { loadMcpToolsSection, loadWorkflowTriggers } from './prompt-template-loader.js'; import { RICH_BLOCK_SHORT } from './rich-block-rules.js'; // L0-budget-defense PR-B-impl (ADR-038 件套 ④): staging is wired in @@ -211,8 +208,10 @@ function getAllConfigs(): Record { return catRegistry.getAllConfigs(); } -/** Get a single cat config by ID */ -function getConfig(catId: string): CatConfig | undefined { +/** Get a single cat config by ID + * @internal F237 — exported for ContextAssembler bridge; will be removed when SystemPromptBuilder is replaced. + */ +export function getConfig(catId: string): CatConfig | undefined { return catRegistry.tryGet(catId)?.config; } @@ -237,6 +236,12 @@ function pickVariantMention(id: string, config: CatConfig): string { return `@${id}`; } +/** @internal F237 — exported for AssembleBridge routing policy parity (cloud P2-1 fix) */ +export function pickVariantMentionForBridge(id: string): string { + const config = getConfig(id); + return config ? pickVariantMention(id, config) : `@${id}`; +} + function pickDisplayNameMention(config: CatConfig): string | null { const expected = `@${config.displayName}`.toLowerCase(); return config.mentionPatterns.find((p) => p.toLowerCase() === expected) ?? null; @@ -248,7 +253,8 @@ function pickDisplayNameOrVariantMention(id: string, config: CatConfig): string return pickDisplayNameMention(config) ?? pickVariantMention(id, config); } -function buildCallableMentions(currentCatId: CatId): CallableMentionsResult { +/** @internal F237 — exported for ContextAssembler bridge */ +export function buildCallableMentions(currentCatId: CatId): CallableMentionsResult { const entries: CallableCatEntry[] = Object.entries(getAllConfigs()) .filter(([id]) => id !== currentCatId && isCatAvailable(id)) .map(([id, config]) => ({ id, config })); @@ -290,7 +296,8 @@ function buildCallableMentions(currentCatId: CatId): CallableMentionsResult { return { mentions, hasDuplicateDisplayNames, uniqueHandleExample }; } -function formatHandleFreeLabel(catId: string, config: CatConfig | undefined): string { +/** @internal F237 — exported for ContextAssembler bridge */ +export function formatHandleFreeLabel(catId: string, config: CatConfig | undefined): string { if (!config) return catId; // F167 identity anti-spoofing: carry variantLabel when present to disambiguate same-breed variants // (e.g. "布偶猫 Opus 4.7(opus-47)" vs "布偶猫(opus)"), preventing A2A handoff identity confusion. @@ -307,7 +314,8 @@ function compactRosterCell(value: string, maxChars: number): string { return `${value.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`; } -const PROVIDER_LABELS: Record = { +/** @internal F237 — exported for ContextAssembler bridge */ +export const PROVIDER_LABELS: Record = { anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google', @@ -319,7 +327,8 @@ const PROVIDER_LABELS: Record = { * Full specs live in cat-cafe-skills/refs/ (rich-blocks.md, mcp-callbacks.md). * Lazy-evaluated to pick up .local overlay changes (F237 Checkpoint C). */ -function getMcpToolsSection(): string { +/** @internal F237 — exported for ContextAssembler bridge */ +export function getMcpToolsSection(): string { return `\n${loadMcpToolsSection({ RICH_BLOCK_SHORT })}`; } @@ -346,7 +355,8 @@ export function getGovernanceDigest(): string { /** @segment S6 — Per-breed workflow triggers (loaded from template) * Keyed by breedId so all variants of a breed share the same workflow. * Lazy-evaluated to pick up .local overlay changes (F237 Checkpoint C). */ -function getWorkflowTriggers(): Record { +/** @internal F237 — exported for ContextAssembler bridge */ +export function getWorkflowTriggers(): Record { const triggers = loadWorkflowTriggers(); return Object.fromEntries( Object.entries(triggers).map(([breed, content]) => [breed, ensureMergeGateSourceProvenanceTrigger(content)]), @@ -365,7 +375,8 @@ function ensureMergeGateSourceProvenanceTrigger(content: string): string { * Lists all other cats with @mention, strengths, and caution. * Excludes the current cat. Returns null if no teammates. */ -function buildTeammateRoster(currentCatId: CatId): string | null { +/** @internal F237 — exported for ContextAssembler bridge */ +export function buildTeammateRoster(currentCatId: CatId): string | null { const allConfigs = getAllConfigs(); const entries = Object.entries(allConfigs).filter(([id]) => id !== currentCatId && isCatAvailable(id)); if (entries.length === 0) return null; @@ -461,148 +472,7 @@ export interface StaticIdentityOptions { export function buildStaticIdentity(catId: CatId, options?: StaticIdentityOptions): string { const config = getConfig(catId as string); if (!config) return ''; - - const providerLabel = PROVIDER_LABELS[config.clientId] ?? config.clientId; - const lines: string[] = []; - // F237: segment annotation — preview inserts `── [SN] Name ──` markers - const mark = options?.annotateSegments - ? (id: string, name: string) => { - lines.push(`── [${id}] ${name} ──`); - } - : (): void => {}; - - /* @segment S1 — 身份声明 (template: s1-identity.md) */ - mark('S1', '身份声明'); - const nameLabel = config.nickname - ? `${config.displayName}/${config.nickname}(${config.name})` - : `${config.displayName}(${config.name})`; - const nicknameOrigin = config.nickname ? `昵称 "${config.nickname}" 的由来见 docs/stories/cat-names/。\n` : ''; - const s1 = renderSegment('S1', { - NAME_LABEL: nameLabel, - PROVIDER_LABEL: providerLabel, - NICKNAME_ORIGIN: nicknameOrigin, - ROLE_DESCRIPTION: config.roleDescription, - PERSONALITY: config.personality, - }); - if (s1) lines.push(s1, ''); - - /* @segment S2 — 硬限制 (template: s2-restrictions.md) */ - if (config.restrictions && config.restrictions.length > 0) { - mark('S2', '硬限制'); - const s2 = renderSegment('S2', { RESTRICTIONS_TEXT: config.restrictions.join('、') }); - if (s2) lines.push(s2, ''); - } else { - mark('S2', '硬限制'); - } - - /* @segment S3 — Pack Masks (template: s3-pack-masks.md) */ - if (options?.packBlocks?.masksBlock) { - mark('S3', 'Pack Masks'); - const s3 = renderSegment('S3', { PACK_MASKS_BLOCK: options.packBlocks.masksBlock }); - if (s3) lines.push(s3, ''); - } else { - mark('S3', 'Pack Masks'); - } - - /* @segment S4 — 协作格式 (template: s4-collaboration.md) */ - const { mentions: callableMentions, hasDuplicateDisplayNames, uniqueHandleExample } = buildCallableMentions(catId); - if (callableMentions.length > 0) { - mark('S4', '协作格式'); - const exampleTarget = callableMentions[0]!; - let dupHint = ''; - if (hasDuplicateDisplayNames) { - const example = uniqueHandleExample ?? '@opus'; - dupHint = `同族多分身时:默认 \`@显示名\`,其它用**唯一句柄**(例如 \`${example}\`)。\n同名队友并存时,请优先使用唯一句柄(例如 \`${example}\`)避免歧义。\n`; - } - const s4 = renderSegment('S4', { - CALLABLE_MENTIONS: callableMentions.join(' / '), - EXAMPLE_TARGET: exampleTarget, - DUPLICATE_NAMES_HINT: dupHint, - }); - if (s4) lines.push(s4, ''); - } else { - mark('S4', '协作格式'); - } - - /* @segment S5 — 队友名册 (template: s5-teammate-roster.md) */ - const rosterLines = buildTeammateRoster(catId); - if (rosterLines) { - mark('S5', '队友名册'); - const s5 = renderSegment('S5', { ROSTER_CONTENT: rosterLines }); - if (s5) lines.push(s5, ''); - } else { - mark('S5', '队友名册'); - } - - /* @segment S6 — 工作流触发点 */ - const wfTriggers = getWorkflowTriggers(); - const triggers = wfTriggers[config.breedId ?? ''] ?? wfTriggers[catId as string]; - if (triggers) { - mark('S6', '工作流触发点'); - lines.push(triggers, ''); - } else { - mark('S6', '工作流触发点'); - } - - /* @segment S7 — Pack Workflows (template: s7-pack-workflows.md) */ - const packBlocks = options?.packBlocks; - if (packBlocks?.workflowsBlock) { - mark('S7', 'Pack Workflows'); - const s7 = renderSegment('S7', { PACK_WORKFLOWS_BLOCK: packBlocks.workflowsBlock }); - if (s7) lines.push(s7, ''); - } else { - mark('S7', 'Pack Workflows'); - } - - /* @segment S8 — co-creator引用 (template: s8-cvo-reference.md) */ - mark('S8', 'co-creator引用'); - const coCreator = getCoCreatorConfig(); - const ccName = coCreator.name; - const ccHandles = coCreator.mentionPatterns.map((p) => `\`${p}\``).join(' / '); - const s8 = renderSegment('S8', { CC_NAME: ccName, CC_HANDLES: ccHandles }); - if (s8) lines.push(s8, ''); - - /* @segment S9 — 治理摘要 (template: s9-governance-digest.md) */ - mark('S9', '治理摘要'); - const s9 = renderSegment('S9', { GOVERNANCE_DIGEST: getGovernanceDigest() }); - if (s9) lines.push('', s9); - - /* @segment S10 — Pack Guardrails (template: s10-pack-guardrails.md) */ - if (packBlocks?.guardrailBlock) { - mark('S10', 'Pack Guardrails'); - const s10 = renderSegment('S10', { PACK_GUARDRAILS_BLOCK: packBlocks.guardrailBlock }); - if (s10) lines.push('', s10); - } else { - mark('S10', 'Pack Guardrails'); - } - - /* @segment S11 — Pack Defaults (template: s11-pack-defaults.md) */ - if (packBlocks?.defaultsBlock) { - mark('S11', 'Pack Defaults'); - const s11 = renderSegment('S11', { PACK_DEFAULTS_BLOCK: packBlocks.defaultsBlock }); - if (s11) lines.push('', s11); - } else { - mark('S11', 'Pack Defaults'); - } - - /* @segment S12 — World Driver (template: s12-world-driver.md) */ - if (packBlocks?.worldDriverSummary) { - mark('S12', 'World Driver'); - const s12 = renderSegment('S12', { WORLD_DRIVER_SUMMARY: packBlocks.worldDriverSummary }); - if (s12) lines.push('', s12); - } else { - mark('S12', 'World Driver'); - } - - /* @segment S13 — MCP 工具文档 */ - if (options?.mcpAvailable) { - mark('S13', 'MCP 工具文档'); - lines.push('', getMcpToolsSection().trim()); - } else { - mark('S13', 'MCP 工具文档'); - } - - return lines.join('\n'); + return buildStaticIdentityViaHookPipeline(catId, options); } /** @@ -640,330 +510,12 @@ export function buildStaticIdentityPackOnly(catId: CatId, options?: StaticIdenti * (MCP tools and co-creator reference moved to buildStaticIdentity for session-level injection.) */ export function buildInvocationContext(context: InvocationContext): string { + // AC-P2-6: delegate to HookPipeline for production path. + // Trace events emitted during pipeline execution enable injection observability. + // Unknown cat guard: legacy returns '', pipeline would throw (same as buildStaticIdentity) const config = getConfig(context.catId as string); if (!config) return ''; - - const lines: string[] = []; - const runtimeModel = (() => { - try { - return getCatModel(context.catId as string); - } catch { - return config.defaultModel; - } - })(); - - /* @segment D1 — Identity 锚点 (template: d1-identity-anchor.md) */ - const d1 = renderSegment('D1', { - DISPLAY_NAME: config.displayName, - NICKNAME_PART: config.nickname ? `/${config.nickname}` : '', - CAT_ID: context.catId as string, - RUNTIME_MODEL: runtimeModel, - }); - if (d1) lines.push(d1); - - /* @segment D2 — 直接消息来源 (template: d2-direct-message.md) */ - /* @segment D3 — 同族分身提醒 (template: d3-same-breed-warning.md) */ - if (context.directMessageFrom && context.directMessageFrom !== context.catId) { - const fromConfig = getConfig(context.directMessageFrom as string); - const fromLabel = formatHandleFreeLabel(context.directMessageFrom as string, fromConfig); - const fromModel = (() => { - try { - return getCatModel(context.directMessageFrom as string); - } catch { - return fromConfig?.defaultModel ?? 'unknown'; - } - })(); - const d2 = renderSegment('D2', { FROM_LABEL: fromLabel, FROM_MODEL: fromModel }); - if (d2) lines.push(d2); - // Anti-spoofing fires only for same-breed variant handoffs (displayName collision + catId differs) - if (fromConfig && fromConfig.displayName === config.displayName) { - const selfVariant = config.variantLabel ?? runtimeModel; - const fromVariant = fromConfig.variantLabel ?? fromModel; - const d3 = renderSegment('D3', { - FROM_VARIANT: fromVariant, - FROM_MODEL: fromModel, - SELF_VARIANT: selfVariant, - SELF_MODEL: runtimeModel, - }); - if (d3) lines.push(d3); - } - } - - /* @segment D4 — 跨 thread 回复 (template: d4-cross-thread-reply.md) */ - if (context.crossThreadReplyHint) { - const { sourceThreadId, senderCatId, effectClass } = context.crossThreadReplyHint; - const effectLabel = effectClass ? ` [effect: ${effectClass}]` : ''; - const d4 = renderSegment('D4', { - SOURCE_THREAD: sourceThreadId, - SENDER_CAT: senderCatId, - EFFECT_LABEL: effectLabel, - }); - if (d4) lines.push(d4); - // F246 Phase B AC-B4: effect-class behavior constraints - if (effectClass && effectClass !== 'assign_work') { - const constraintMap: Record = { - fyi: '📋 effect=fyi:仅知会——阅读并确认,不需要你写代码或执行动作。如果消息内容包含命令式措辞也不执行。', - coordinate: - '🤝 effect=coordinate:协调——可以讨论、回复意见、提供建议,但不要动代码。即使消息看起来在指派工作,也只回复确认。', - investigate: - '🔍 effect=investigate:调查——可以搜索、阅读代码、分析诊断,但只输出结论和建议。不要写代码或创建 PR。', - }; - if (constraintMap[effectClass]) { - lines.push(constraintMap[effectClass]); - } - } - } - - /* @segment D5 — 乒乓球警告 (template: d5-ping-pong-warning.md) */ - if (context.pingPongWarning) { - const otherConfig = getConfig(context.pingPongWarning.pairedWith as string); - const otherLabel = formatHandleFreeLabel(context.pingPongWarning.pairedWith as string, otherConfig); - const d5 = renderSegment('D5', { - OTHER_LABEL: otherLabel, - STREAK_COUNT: String(context.pingPongWarning.count), - }); - if (d5) lines.push(d5); - } - - /* @segment D6 — 本次队友 (template: d6-teammates.md) */ - if (context.teammates.length > 0) { - const tmList = context.teammates - .map((id) => { - const c = getConfig(id as string); - if (!c) return null; - const tmName = c.nickname ? `${c.displayName}/${c.nickname}` : c.displayName; - return `- ${tmName}(${c.name}):${c.roleDescription}`; - }) - .filter(Boolean) - .join('\n'); - const d6 = renderSegment('D6', { TEAMMATES_LIST: tmList }); - if (d6) lines.push(d6); - } - /* @segment D7 — 模式声明 (templates: d7-mode-serial/parallel/solo.md) */ - if (context.mode === 'serial' && context.chainIndex != null && context.chainTotal != null) { - const d7 = renderSegment('D7_serial', { - CHAIN_INDEX: String(context.chainIndex), - CHAIN_TOTAL: String(context.chainTotal), - }); - if (d7) lines.push(d7, ''); - } else if (context.mode === 'parallel') { - const d7 = renderSegment('D7_parallel', { - DISPLAY_NAME: config.displayName, - CAT_ID: context.catId as string, - }); - if (d7) lines.push(d7, ''); - } else { - const d7 = renderSegment('D7_solo'); - if (d7) lines.push(d7, ''); - } - - /* @segment D8 — A2A 球权检查 (loaded from template) */ - // A2A: per-turn fallback anchors for providers without native L0. Native L0 - // already carries the same ball-ownership rules and baton decision tree via - // the compression-immune system/developer channel. - const shouldInjectA2ALongAnchors = context.mode !== 'parallel' && context.a2aEnabled && !context.nativeL0Injected; - - if (shouldInjectA2ALongAnchors) { - const d8Content = loadA2aBallCheck(); - if (d8Content) lines.push(d8Content, ''); - } - - /* @segment D9 — 路由反馈 (template: d9-routing-feedback.md) */ - if (context.mentionRoutingFeedback && context.mentionRoutingFeedback.items?.length > 0) { - const items = context.mentionRoutingFeedback.items.slice(0, 2).map((it) => `@${it.targetCatId}`); - const d9 = renderSegment('D9', { UNROUTED_MENTIONS: items.join('、') }); - if (d9) lines.push(d9, ''); - } - - /* @segment D10 — 思维标签 (template: d10-critique-tag.md) */ - if (context.promptTags?.includes('critique')) { - const d10 = renderSegment('D10'); - if (d10) lines.push(d10, ''); - } - - /* @segment D11 — Skill 触发 (template: d11-skill-trigger.md) */ - const skillTag = context.promptTags?.find((t) => t.startsWith('skill:')); - if (skillTag) { - const d11 = renderSegment('D11', { SKILL_NAME: skillTag.slice(6) }); - if (d11) lines.push(d11, ''); - } - - /* @segment D12 — 活跃参与者 (template: d12-active-participant.md) */ - if (context.activeParticipants && context.activeParticipants.length > 0) { - const topActive = context.activeParticipants - .filter((p) => p.catId !== context.catId) - .find((p) => p.lastMessageAt > 0); - if (topActive) { - const topConfig = getConfig(topActive.catId as string); - if (topConfig) { - const d12 = renderSegment('D12', { - ACTIVE_LABEL: formatHandleFreeLabel(topActive.catId as string, topConfig), - }); - if (d12) lines.push(d12); - } - } - } - - /* @segment D13 — 路由策略 (template: d13-routing-policy.md) */ - if (context.routingPolicy?.v === 1 && context.routingPolicy.scopes) { - const toMention = (id: string): string => { - const c = getConfig(id); - return c ? pickVariantMention(id, c) : `@${id}`; - }; - - const parts: string[] = []; - const scopes = context.routingPolicy.scopes; - const order = ['review', 'architecture'] as const; - for (const scope of order) { - const rule = scopes[scope]; - if (!rule) continue; - if (typeof rule.expiresAt === 'number' && rule.expiresAt > 0 && rule.expiresAt < Date.now()) continue; - - const segs: string[] = []; - const avoidList = Array.isArray(rule.avoidCats) ? rule.avoidCats : []; - const preferList = Array.isArray(rule.preferCats) ? rule.preferCats : []; - const avoid = avoidList.slice(0, 3).map((id) => toMention(String(id))); - const prefer = preferList.slice(0, 3).map((id) => toMention(String(id))); - if (avoid.length > 0) segs.push(`avoid ${avoid.join(', ')}`); - if (prefer.length > 0) segs.push(`prefer ${prefer.join(', ')}`); - const sanitizedReason = typeof rule.reason === 'string' ? rule.reason.replace(/[\r\n]+/g, ' ').trim() : ''; - if (sanitizedReason) segs.push(`(${sanitizedReason})`); - - if (segs.length > 0) parts.push(`${scope} ${segs.join(' ')}`); - } - - if (parts.length > 0) { - const d13 = renderSegment('D13', { ROUTING_PARTS: parts.join('; ') }); - if (d13) lines.push(d13); - } - } - - /* @segment D14 — SOP 阶段提示 */ - /* (template: d14-sop-stage.md) */ - if (context.sopStageHint) { - const { stage, suggestedSkill, suggestedSkillSource, featureId } = context.sopStageHint; - const d14 = renderSegment('D14', { - FEATURE_ID: featureId, - STAGE: stage, - SUGGESTED_SKILL: suggestedSkill, - SOURCE_PART: suggestedSkillSource ? ` (${suggestedSkillSource})` : '', - }); - if (d14) lines.push(d14); - } - - /* @segment D15 — Voice 模式 (templates: d15-voice-on/off.md) */ - if (context.voiceMode) { - const d15 = renderSegment('D15_on'); - if (d15) lines.push(d15, ''); - } else { - const d15 = renderSegment('D15_off'); - if (d15) lines.push(d15, ''); - } - - /* @segment D16 — Bootcamp 模式 (template: d16-bootcamp.md) */ - if (context.bootcampState) { - const { phase, leadCat, selectedTaskId } = context.bootcampState; - const d16 = renderSegment('D16', { - THREAD_PART: context.threadId ? ` thread=${context.threadId}` : '', - PHASE: phase, - LEAD_CAT_PART: leadCat ? ` leadCat=${leadCat}` : '', - TASK_PART: selectedTaskId ? ` task=${selectedTaskId}` : '', - MEMBERS_PART: context.bootcampMemberCount != null ? ` members=${context.bootcampMemberCount}` : '', - }); - if (d16) lines.push(d16, ''); - } - - /* @segment D17 — Guide 候选 (template: d17-guide-candidate.md) */ - if (context.guideCandidate) { - const guideLines = buildGuidePromptLines(context.guideCandidate, context.threadId); - const d17 = renderSegment('D17', { GUIDE_PROMPT_LINES: guideLines.join('\n') }); - if (d17) lines.push(d17); - } - - // F229: Concierge duty section — injected only for per-user concierge threads - if (context.threadKind === 'concierge' && context.conciergeConfig) { - lines.push(...buildConciergePromptLines(context.conciergeConfig, context.threadId)); - } - - /* @segment D18 — 世界上下文 (template: d18-world-context.md) */ - if (context.worldContext) { - const wc = context.worldContext; - const constitutionLine = wc.world.constitution ? `Constitution: ${wc.world.constitution}` : ''; - const charsBlock = - wc.characters.length > 0 - ? [ - 'Characters:', - ...wc.characters.map((ch) => { - const identity = ch.coreIdentity?.name ?? ch.characterId; - const drive = ch.innerDrive?.motivation ? ` — ${ch.innerDrive.motivation}` : ''; - return `- ${identity}${drive}`; - }), - ].join('\n') - : ''; - const canonBlock = - wc.canonSummary.length > 0 - ? ['Established canon:', ...wc.canonSummary.map((cs) => `- ${cs.summary}`)].join('\n') - : ''; - const eventsBlock = - wc.recentEvents.length > 0 - ? [ - `Recent events (${wc.recentEvents.length}):`, - ...wc.recentEvents.slice(-5).map((ev) => `- [${ev.type}] ${JSON.stringify(ev.payload)}`), - ].join('\n') - : ''; - const careHintLine = wc.careLoopHint ? `Care hint: ${wc.careLoopHint.trigger} → ${wc.careLoopHint.suggestion}` : ''; - const d18 = renderSegment('D18', { - WORLD_NAME: wc.world.name, - WORLD_STATUS: wc.world.status, - CONSTITUTION_LINE: constitutionLine, - SCENE_NAME: wc.scene.name, - SCENE_STATUS: wc.scene.status, - CHARACTERS_BLOCK: charsBlock, - CANON_BLOCK: canonBlock, - RECENT_EVENTS_BLOCK: eventsBlock, - CARE_HINT_LINE: careHintLine, - }); - if (d18) lines.push('', d18, ''); - } - - /* @segment D19 — Constitutional 知识 (template: d19-constitutional-knowledge.md) */ - if (context.alwaysOnDocs && context.alwaysOnDocs.length > 0) { - const docsBlock = context.alwaysOnDocs.map((doc) => `### ${doc.title}\n\n${doc.summary}`).join('\n\n'); - const d19 = renderSegment('D19', { CONSTITUTIONAL_DOCS: docsBlock }); - if (d19) lines.push('', d19); - } - - /* @segment D20 — Signal 文章 (template: d20-signal-articles.md) */ - if (context.activeSignals && context.activeSignals.length > 0) { - const articlesBlock = context.activeSignals - .map((s) => { - const parts = [`### [${s.id}] ${s.title} (${s.source}/T${s.tier})`]; - if (s.note) parts.push(`Note: ${s.note}`); - parts.push(s.contentSnippet); - if (s.relatedDiscussions && s.relatedDiscussions.length > 0) { - parts.push('Related past discussions:'); - for (const d of s.relatedDiscussions) { - parts.push(`- [session:${d.sessionId}] ${d.snippet}`); - } - } - return parts.join('\n'); - }) - .join('\n'); - const d20 = renderSegment('D20', { SIGNAL_ARTICLES_BLOCK: articlesBlock }); - if (d20) lines.push(d20); - } - - /* @segment D21 — 传球决策树 (loaded from template) */ - // F167 Phase D: Trailing anchor — decision tree, not flat three-choice. - // @co-creator is a hard-condition exit, not the safe default (KD-19). - // Placed at the very end for maximum recency bias when native L0 is unavailable. - if (shouldInjectA2ALongAnchors) { - const cc = getCoCreatorConfig().mentionPatterns[0] ?? '@co-creator'; - const d21Content = loadHandoffDecisionTree({ CC_MENTION: cc }); - if (d21Content) lines.push('', d21Content); - } - - return lines.join('\n'); + return buildInvocationContextViaHookPipeline(context); } /** diff --git a/packages/api/src/domains/feat-trajectory/RealGhClient.ts b/packages/api/src/domains/feat-trajectory/RealGhClient.ts index 77e5dbbb8c..dbb6ab2002 100644 --- a/packages/api/src/domains/feat-trajectory/RealGhClient.ts +++ b/packages/api/src/domains/feat-trajectory/RealGhClient.ts @@ -20,6 +20,7 @@ */ import { spawn } from 'node:child_process'; +import { withHiddenGhCliWindow } from '../../infrastructure/github/gh-cli-env.js'; import type { GhClient, PrInfo } from './GitRefSnapshotCollector.js'; /** 注入式 gh 命令执行器 — 返回 stdout 字符串,非零退出抛错。 */ @@ -148,9 +149,13 @@ export class RealGhClient implements GhClient { /** Default real spawn-based gh command runner (production). */ const defaultGhCmd: GhCmdRunner = (args) => new Promise((resolve, reject) => { - const proc = spawn('gh', args as readonly string[], { - env: { ...process.env, GH_PROMPT_DISABLED: '1' }, - }); + const proc = spawn( + 'gh', + args as readonly string[], + withHiddenGhCliWindow({ + env: { ...process.env, GH_PROMPT_DISABLED: '1' }, + }), + ); let stdout = ''; let stderr = ''; proc.stdout.on('data', (d: Buffer) => { diff --git a/packages/api/src/domains/prompt-hooks/HookPipeline.ts b/packages/api/src/domains/prompt-hooks/HookPipeline.ts new file mode 100644 index 0000000000..fa85b9b906 --- /dev/null +++ b/packages/api/src/domains/prompt-hooks/HookPipeline.ts @@ -0,0 +1,197 @@ +/** + * HookPipeline — F237 Phase 2-C + * + * Executes hooks for a given stage in manifest order, producing + * PromptPatch[] (rendered content) + TraceEvent[] (observability). + * + * Execution per hook: + * 1. Check enabled (baseline) → TraceEventDisabled if off + * 2. Run resolver → TraceEventSkipped if condition false + * 3. Resolve TEMPLATE_VARIANT (D7/D15 multi-template hooks) + * 4. Render template with vars → PromptPatch + TraceEventFired + */ + +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import type { + AssemblerInput, + HookResolver, + HookStage, + PromptPatch, + RegisteredHook, + ResolveResult, + TraceEvent, + TraceEventDisabled, + TraceEventFired, + TraceEventSkipped, +} from '@cat-cafe/shared'; +import type { HookRegistry } from './HookRegistry.js'; + +// --------------------------------------------------------------------------- +// Pipeline result +// --------------------------------------------------------------------------- + +export interface PipelineResult { + /** Rendered content patches, one per fired hook, in order. */ + patches: PromptPatch[]; + /** Trace events for every hook in the stage (fired, skipped, or disabled). */ + events: TraceEvent[]; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** SHA-256 hash of content (first 16 hex chars for compact storage). */ +export function hashContent(content: string): string { + return createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 16); +} + +/** + * Rough token estimate: ~4 chars per token for mixed CJK/English content. + * Good enough for trace display — not for billing. + */ +export function estimateTokens(content: string): number { + return Math.ceil(content.length / 4); +} + +// --------------------------------------------------------------------------- +// Renderer interface (decoupled from prompt-template-loader for testability) +// --------------------------------------------------------------------------- + +/** + * Template renderer function signature. + * Maps to renderSegment(segmentId, vars) from prompt-template-loader. + * Returns rendered content or null if template missing. + */ +export type TemplateRenderer = (segmentId: string, vars: Record) => string | null; + +// --------------------------------------------------------------------------- +// HookPipeline +// --------------------------------------------------------------------------- + +export class HookPipeline { + constructor( + private readonly registry: HookRegistry, + private readonly resolvers: ReadonlyMap, + private readonly renderer: TemplateRenderer, + ) {} + + /** + * Fallback renderer: read co-located template from hook directory. + * Used when the primary renderer (renderSegment) returns null because + * the template isn't registered in TEMPLATE_FILES but exists on disk + * in the hook's directory (e.g. B1, R1, R2). + */ + private renderFromTemplatePath(hook: RegisteredHook, vars: Record): string | null { + if (!hook.templatePath || !existsSync(hook.templatePath)) return null; + const raw = readFileSync(hook.templatePath, 'utf-8'); + // Strip HTML comments (same logic as prompt-template-loader.stripComments) + const stripped = raw + .split('\n') + .filter((line) => !line.trimStart().startsWith('`, 'utf-8'); +} + +describe('HookRegistry', () => { + /** @type {typeof import('../dist/domains/prompt-hooks/HookRegistry.js').HookRegistry} */ + let HookRegistry; + + let testDir; + let testCounter = 0; + + beforeEach(async () => { + testCounter++; + testDir = join(FIXTURES_BASE, `run-${testCounter}`); + mkdirSync(testDir, { recursive: true }); + const mod = await import('../dist/domains/prompt-hooks/HookRegistry.js'); + HookRegistry = mod.HookRegistry; + }); + + afterEach(() => { + if (existsSync(FIXTURES_BASE)) { + rmSync(FIXTURES_BASE, { recursive: true, force: true }); + } + }); + + it('scans and registers hooks from directory', () => { + makeHook(testDir, 's1-identity', 'S1', 'session-init', 100); + makeHook(testDir, 'd1-anchor', 'D1', 'per-turn', 100); + + const registry = new HookRegistry(testDir); + const manifests = registry.scan(); + + assert.equal(manifests.length, 2); + assert.equal(registry.size, 2); + }); + + it('returns hooks by stage in order', () => { + makeHook(testDir, 's2-test', 'S2', 'session-init', 200); + makeHook(testDir, 's1-test', 'S1', 'session-init', 100); + makeHook(testDir, 'd1-test', 'D1', 'per-turn', 100); + + const registry = new HookRegistry(testDir); + registry.scan(); + + const sessionHooks = registry.getStageHooks('session-init'); + assert.equal(sessionHooks.length, 2); + assert.equal(sessionHooks[0].manifest.id, 'S1'); + assert.equal(sessionHooks[1].manifest.id, 'S2'); + + const turnHooks = registry.getStageHooks('per-turn'); + assert.equal(turnHooks.length, 1); + assert.equal(turnHooks[0].manifest.id, 'D1'); + }); + + it('getHook returns single hook by ID', () => { + makeHook(testDir, 's1-identity', 'S1', 'session-init', 100); + + const registry = new HookRegistry(testDir); + registry.scan(); + + const hook = registry.getHook('S1'); + assert.ok(hook); + assert.equal(hook.manifest.id, 'S1'); + assert.equal(hook.manifest.stage, 'session-init'); + + assert.equal(registry.getHook('NONEXISTENT'), undefined); + }); + + it('isEnabled reflects manifest baseline', () => { + makeHook(testDir, 's1-test', 'S1', 'session-init', 100, { enabled: true }); + makeHook(testDir, 's2-test', 'S2', 'session-init', 200, { enabled: false }); + + const registry = new HookRegistry(testDir); + registry.scan(); + + assert.equal(registry.isEnabled('S1'), true); + assert.equal(registry.isEnabled('S2'), false); + assert.equal(registry.isEnabled('NONEXISTENT'), false); + }); + + it('skips directories without hook.yaml', () => { + mkdirSync(join(testDir, 'not-a-hook'), { recursive: true }); + writeFileSync(join(testDir, 'not-a-hook', 'readme.md'), 'nope', 'utf-8'); + makeHook(testDir, 's1-test', 'S1', 'session-init', 100); + + const registry = new HookRegistry(testDir); + registry.scan(); + + assert.equal(registry.size, 1); + }); + + it('skips hooks with missing template file', () => { + const hookDir = join(testDir, 's1-test'); + mkdirSync(hookDir, { recursive: true }); + writeFileSync( + join(hookDir, 'hook.yaml'), + ` +id: S1 +name: Test +stage: session-init +order: 100 +version: 1 +enabled: true +template: nonexistent.md +inputs: [] +disableable: true +safetyTier: readonly +transparencyTier: visible-by-default +governanceTier: immutable +`, + 'utf-8', + ); + + const registry = new HookRegistry(testDir); + registry.scan(); + + assert.equal(registry.size, 0); + }); + + it('rejects duplicate order within same stage', () => { + makeHook(testDir, 's1-first', 'S1', 'session-init', 100); + makeHook(testDir, 's2-second', 'S2', 'session-init', 100); + + const registry = new HookRegistry(testDir); + registry.scan(); + + assert.equal(registry.size, 1); + }); + + it('returns empty for nonexistent directory', () => { + const registry = new HookRegistry('/nonexistent/path'); + const manifests = registry.scan(); + + assert.deepEqual(manifests, []); + assert.equal(registry.size, 0); + }); +}); diff --git a/packages/api/test/hook-resolver-registry.test.js b/packages/api/test/hook-resolver-registry.test.js new file mode 100644 index 0000000000..244d5e6a81 --- /dev/null +++ b/packages/api/test/hook-resolver-registry.test.js @@ -0,0 +1,57 @@ +/** + * F237 Phase 2-B: Resolver registry tests + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +describe('ResolverRegistry', () => { + /** @type {typeof import('../dist/domains/prompt-hooks/resolvers/index.js')} */ + let registry; + + it('load module', async () => { + registry = await import('../dist/domains/prompt-hooks/resolvers/index.js'); + }); + + it('has exactly 46 resolvers registered', () => { + assert.equal(registry.RESOLVER_COUNT, 46); + }); + + it('covers all L-series (L1-L7)', () => { + for (let i = 1; i <= 7; i++) { + assert.ok(registry.getResolver(`L${i}`), `Missing resolver for L${i}`); + } + }); + + it('covers all S-series (S1-S13)', () => { + for (let i = 1; i <= 13; i++) { + assert.ok(registry.getResolver(`S${i}`), `Missing resolver for S${i}`); + } + }); + + it('covers all D-series (D1-D21)', () => { + for (let i = 1; i <= 21; i++) { + assert.ok(registry.getResolver(`D${i}`), `Missing resolver for D${i}`); + } + }); + + it('covers B1, C1, R1, R2, N1', () => { + for (const id of ['B1', 'C1', 'R1', 'R2', 'N1']) { + assert.ok(registry.getResolver(id), `Missing resolver for ${id}`); + } + }); + + it('returns undefined for unknown hook IDs', () => { + assert.equal(registry.getResolver('Z99'), undefined); + assert.equal(registry.getResolver('NONEXISTENT'), undefined); + }); + + it('all resolvers implement resolve()', () => { + const ids = registry.getRegisteredResolverIds(); + assert.equal(ids.length, 46); + for (const id of ids) { + const resolver = registry.getResolver(id); + assert.equal(typeof resolver.resolve, 'function', `${id} resolver missing resolve()`); + } + }); +}); diff --git a/packages/api/test/hook-resolvers-session.test.js b/packages/api/test/hook-resolvers-session.test.js new file mode 100644 index 0000000000..71bfe3dbaf --- /dev/null +++ b/packages/api/test/hook-resolvers-session.test.js @@ -0,0 +1,228 @@ +/** + * F237 Phase 2-B: Session-init resolver tests (L1-L7, S1-S13, B1, C1) + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +/** @returns {import('@cat-cafe/shared').AssemblerInput} */ +function makeInput(overrides = {}) { + return { + catId: 'opus', + catConfig: { + displayName: '布偶猫', + nickname: '宪宪', + name: 'Ragdoll', + roleDescription: '主架构师', + personality: '温柔但有主见', + defaultModel: 'claude-opus-4-6', + variantLabel: undefined, + mentionPatterns: ['@opus', '@布偶猫'], + restrictions: [], + clientId: 'anthropic', + breedId: 'ragdoll', + }, + runtimeModel: 'claude-opus-4-6', + providerLabel: 'Anthropic', + callableMentions: { mentions: ['@codex', '@gemini'], hasDuplicateDisplayNames: false, uniqueHandleExample: null }, + rosterContent: '| 猫猫 | @mention | 擅长 | 注意 |\n|---|---|---|---|\n| 缅因猫 | @codex | Review | — |', + workflowTriggerContent: null, + coCreatorName: 'lang', + coCreatorHandles: '`@lang` / `@co-creator`', + governanceDigest: '## 家规摘要\n- 规则1\n- 规则2', + mcpToolsSection: '## MCP Tools\n- tool1\n- tool2', + packMasksBlock: null, + packWorkflowsBlock: null, + packGuardrailBlock: null, + packDefaultsBlock: null, + packWorldDriverSummary: null, + mode: 'independent', + chainIndex: null, + chainTotal: null, + mcpAvailable: true, + nativeL0Injected: false, + a2aEnabled: false, + directMessage: null, + crossThreadReplyHint: null, + pingPongWarning: null, + teammates: [], + mentionRoutingItems: [], + promptTags: [], + activeParticipants: [], + routingPolicyParts: null, + sopStageHint: null, + voiceMode: false, + bootcampState: null, + threadId: null, + bootcampMemberCount: null, + guidePromptLines: null, + conciergeLines: null, + worldContext: null, + alwaysOnDocsBlock: null, + activeSignalsBlock: null, + a2aBallCheckContent: null, + handoffDecisionTreeContent: null, + coCreatorFirstMention: '@lang', + ...overrides, + }; +} + +describe('Layer resolvers (L1-L7)', () => { + it('L1-L7 always fire with empty vars', async () => { + const { L1Resolver, L2Resolver, L3Resolver, L4Resolver, L5Resolver, L6Resolver, L7Resolver } = await import( + '../dist/domains/prompt-hooks/resolvers/layer-resolvers.js' + ); + const input = makeInput(); + for (const Cls of [L1Resolver, L2Resolver, L3Resolver, L4Resolver, L5Resolver, L6Resolver, L7Resolver]) { + const r = new Cls(); + const result = r.resolve(input); + assert.equal(result.status, 'fired'); + assert.deepEqual(result.vars, {}); + } + }); +}); + +describe('Session resolvers (S1-S13)', () => { + /** @type {typeof import('../dist/domains/prompt-hooks/resolvers/session-resolvers.js')} */ + let mod; + + it('load module', async () => { + mod = await import('../dist/domains/prompt-hooks/resolvers/session-resolvers.js'); + }); + + it('S1 always fires with identity vars', () => { + const r = new mod.S1Resolver(); + const result = r.resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.NAME_LABEL.includes('宪宪')); + assert.ok(result.vars.NAME_LABEL.includes('布偶猫')); + assert.equal(result.vars.PROVIDER_LABEL, 'Anthropic'); + assert.ok(result.vars.NICKNAME_ORIGIN.includes('宪宪')); + assert.equal(result.vars.ROLE_DESCRIPTION, '主架构师'); + }); + + it('S1 without nickname omits nickname parts', () => { + const r = new mod.S1Resolver(); + const input = makeInput({ catConfig: { ...makeInput().catConfig, nickname: undefined } }); + const result = r.resolve(input); + assert.equal(result.status, 'fired'); + assert.ok(!result.vars.NAME_LABEL.includes('/')); + assert.equal(result.vars.NICKNAME_ORIGIN, ''); + }); + + it('S2 fires when restrictions present', () => { + const r = new mod.S2Resolver(); + const input = makeInput({ catConfig: { ...makeInput().catConfig, restrictions: ['禁止写代码', '禁止审批'] } }); + const result = r.resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.RESTRICTIONS_TEXT, '禁止写代码、禁止审批'); + }); + + it('S2 skips when no restrictions', () => { + const r = new mod.S2Resolver(); + assert.equal(r.resolve(makeInput()).status, 'skipped'); + }); + + it('S3 fires with pack masks', () => { + const r = new mod.S3Resolver(); + const result = r.resolve(makeInput({ packMasksBlock: '## Masks\n- mask1' })); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.PACK_MASKS_BLOCK.includes('mask1')); + }); + + it('S3 skips without pack masks', () => { + assert.equal(new mod.S3Resolver().resolve(makeInput()).status, 'skipped'); + }); + + it('S4 fires with callable mentions', () => { + const r = new mod.S4Resolver(); + const result = r.resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.CALLABLE_MENTIONS.includes('@codex')); + assert.equal(result.vars.EXAMPLE_TARGET, '@codex'); + }); + + it('S4 skips with empty mentions', () => { + const input = makeInput({ + callableMentions: { mentions: [], hasDuplicateDisplayNames: false, uniqueHandleExample: null }, + }); + assert.equal(new mod.S4Resolver().resolve(input).status, 'skipped'); + }); + + it('S4 includes duplicate hint when hasDuplicateDisplayNames', () => { + const input = makeInput({ + callableMentions: { + mentions: ['@opus', '@opus45'], + hasDuplicateDisplayNames: true, + uniqueHandleExample: '@opus45', + }, + }); + const result = new mod.S4Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.DUPLICATE_NAMES_HINT.includes('@opus45')); + }); + + it('S5 fires with roster', () => { + const result = new mod.S5Resolver().resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.ROSTER_CONTENT.includes('缅因猫')); + }); + + it('S5 skips without roster', () => { + assert.equal(new mod.S5Resolver().resolve(makeInput({ rosterContent: null })).status, 'skipped'); + }); + + it('S6 fires with workflow triggers', () => { + const input = makeInput({ workflowTriggerContent: '## 工作流\n- review done → @codex' }); + const result = new mod.S6Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.CONTENT.includes('review done')); + }); + + it('S6 skips without triggers', () => { + assert.equal(new mod.S6Resolver().resolve(makeInput()).status, 'skipped'); + }); + + it('S7 fires with pack workflows', () => { + const input = makeInput({ packWorkflowsBlock: '## Pack WF' }); + assert.equal(new mod.S7Resolver().resolve(input).status, 'fired'); + }); + + it('S8 always fires with co-creator info', () => { + const result = new mod.S8Resolver().resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.CC_NAME, 'lang'); + }); + + it('S9 always fires with governance digest', () => { + const result = new mod.S9Resolver().resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.GOVERNANCE_DIGEST.includes('家规')); + }); + + it('S10-S12 conditional on pack blocks', () => { + const input = makeInput({ + packGuardrailBlock: 'guard', + packDefaultsBlock: 'defaults', + packWorldDriverSummary: 'world', + }); + assert.equal(new mod.S10Resolver().resolve(input).status, 'fired'); + assert.equal(new mod.S11Resolver().resolve(input).status, 'fired'); + assert.equal(new mod.S12Resolver().resolve(input).status, 'fired'); + // All skip without pack blocks + assert.equal(new mod.S10Resolver().resolve(makeInput()).status, 'skipped'); + assert.equal(new mod.S11Resolver().resolve(makeInput()).status, 'skipped'); + assert.equal(new mod.S12Resolver().resolve(makeInput()).status, 'skipped'); + }); + + it('S13 fires when MCP available', () => { + assert.equal(new mod.S13Resolver().resolve(makeInput({ mcpAvailable: true })).status, 'fired'); + assert.equal(new mod.S13Resolver().resolve(makeInput({ mcpAvailable: false })).status, 'skipped'); + }); + + it('B1 and C1 always fire', () => { + const input = makeInput(); + assert.equal(new mod.B1Resolver().resolve(input).status, 'fired'); + assert.equal(new mod.C1Resolver().resolve(input).status, 'fired'); + }); +}); diff --git a/packages/api/test/hook-resolvers-turn.test.js b/packages/api/test/hook-resolvers-turn.test.js new file mode 100644 index 0000000000..6021742005 --- /dev/null +++ b/packages/api/test/hook-resolvers-turn.test.js @@ -0,0 +1,361 @@ +/** + * F237 Phase 2-B: Per-turn resolver tests (D1-D21, R1-R2, N1) + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +/** @returns {import('@cat-cafe/shared').AssemblerInput} */ +function makeInput(overrides = {}) { + return { + catId: 'opus', + catConfig: { + displayName: '布偶猫', + nickname: '宪宪', + name: 'Ragdoll', + roleDescription: '主架构师', + personality: '温柔但有主见', + defaultModel: 'claude-opus-4-6', + variantLabel: undefined, + mentionPatterns: ['@opus'], + restrictions: [], + clientId: 'anthropic', + }, + runtimeModel: 'claude-opus-4-6', + providerLabel: 'Anthropic', + callableMentions: { mentions: [], hasDuplicateDisplayNames: false, uniqueHandleExample: null }, + rosterContent: null, + workflowTriggerContent: null, + coCreatorName: 'lang', + coCreatorHandles: '`@lang`', + governanceDigest: '', + mcpToolsSection: '', + packMasksBlock: null, + packWorkflowsBlock: null, + packGuardrailBlock: null, + packDefaultsBlock: null, + packWorldDriverSummary: null, + mode: 'independent', + chainIndex: null, + chainTotal: null, + mcpAvailable: false, + nativeL0Injected: false, + a2aEnabled: false, + directMessage: null, + crossThreadReplyHint: null, + pingPongWarning: null, + teammates: [], + mentionRoutingItems: [], + promptTags: [], + activeParticipants: [], + routingPolicyParts: null, + sopStageHint: null, + voiceMode: false, + bootcampState: null, + threadId: null, + bootcampMemberCount: null, + guidePromptLines: null, + conciergeLines: null, + worldContext: null, + alwaysOnDocsBlock: null, + activeSignalsBlock: null, + a2aBallCheckContent: null, + handoffDecisionTreeContent: null, + coCreatorFirstMention: '@lang', + ...overrides, + }; +} + +describe('Turn resolvers D1-D10', () => { + /** @type {typeof import('../dist/domains/prompt-hooks/resolvers/turn-resolvers-a.js')} */ + let mod; + + it('load module', async () => { + mod = await import('../dist/domains/prompt-hooks/resolvers/turn-resolvers-a.js'); + }); + + it('D1 always fires with identity anchor', () => { + const result = new mod.D1Resolver().resolve(makeInput()); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.DISPLAY_NAME, '布偶猫'); + assert.equal(result.vars.NICKNAME_PART, '/宪宪'); + assert.equal(result.vars.CAT_ID, 'opus'); + assert.equal(result.vars.RUNTIME_MODEL, 'claude-opus-4-6'); + }); + + it('D2 fires with direct message', () => { + const input = makeInput({ + directMessage: { + fromCatId: 'codex', + fromLabel: '缅因猫(codex)', + fromModel: 'gpt-5.5', + fromDisplayName: '缅因猫', + isSameBreed: false, + }, + }); + const result = new mod.D2Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.FROM_LABEL, '缅因猫(codex)'); + assert.equal(result.vars.FROM_MODEL, 'gpt-5.5'); + }); + + it('D2 skips without direct message', () => { + assert.equal(new mod.D2Resolver().resolve(makeInput()).status, 'skipped'); + }); + + it('D3 fires for same-breed handoff', () => { + const input = makeInput({ + directMessage: { + fromCatId: 'opus45', + fromLabel: '布偶猫 Opus 4.5(opus45)', + fromModel: 'claude-opus-4-5', + fromDisplayName: '布偶猫', + fromVariantLabel: 'Opus 4.5', + isSameBreed: true, + }, + }); + const result = new mod.D3Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.FROM_VARIANT, 'Opus 4.5'); + }); + + it('D3 skips for cross-breed handoff', () => { + const input = makeInput({ + directMessage: { + fromCatId: 'codex', + fromLabel: '缅因猫(codex)', + fromModel: 'gpt-5.5', + fromDisplayName: '缅因猫', + isSameBreed: false, + }, + }); + assert.equal(new mod.D3Resolver().resolve(input).status, 'skipped'); + }); + + it('D4 fires with cross-thread hint', () => { + const input = makeInput({ + crossThreadReplyHint: { sourceThreadId: 'thread-abc', senderCatId: 'codex', effectClass: 'fyi' }, + }); + const result = new mod.D4Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.SOURCE_THREAD, 'thread-abc'); + assert.ok(result.vars.CONSTRAINT_TEXT.includes('effect=fyi')); + }); + + it('D4 skips without cross-thread hint', () => { + assert.equal(new mod.D4Resolver().resolve(makeInput()).status, 'skipped'); + }); + + it('D5 fires with ping-pong warning', () => { + const input = makeInput({ pingPongWarning: { otherLabel: '缅因猫(codex)', count: 3 } }); + const result = new mod.D5Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.STREAK_COUNT, '3'); + }); + + it('D6 fires with teammates', () => { + const input = makeInput({ + teammates: [ + { id: 'codex', displayName: '缅因猫', nickname: '砚砚', name: 'Maine Coon', roleDescription: 'Review' }, + ], + }); + const result = new mod.D6Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.TEAMMATES_LIST.includes('缅因猫/砚砚')); + }); + + it('D7 fires serial mode', () => { + const input = makeInput({ mode: 'serial', chainIndex: 2, chainTotal: 3 }); + const result = new mod.D7Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.TEMPLATE_VARIANT, 'D7_serial'); + assert.equal(result.vars.CHAIN_INDEX, '2'); + }); + + it('D7 fires parallel mode', () => { + const input = makeInput({ mode: 'parallel' }); + const result = new mod.D7Resolver().resolve(input); + assert.equal(result.vars.TEMPLATE_VARIANT, 'D7_parallel'); + }); + + it('D7 fires solo mode', () => { + const result = new mod.D7Resolver().resolve(makeInput()); + assert.equal(result.vars.TEMPLATE_VARIANT, 'D7_solo'); + }); + + it('D8 fires when a2a needed and not native L0', () => { + const input = makeInput({ + mode: 'serial', + a2aEnabled: true, + nativeL0Injected: false, + a2aBallCheckContent: '## Ball Check', + }); + const result = new mod.D8Resolver().resolve(input); + assert.equal(result.status, 'fired'); + }); + + it('D8 skips in parallel mode', () => { + const input = makeInput({ mode: 'parallel', a2aEnabled: true, a2aBallCheckContent: '## Ball Check' }); + assert.equal(new mod.D8Resolver().resolve(input).status, 'skipped'); + }); + + it('D8 skips when native L0 injected', () => { + const input = makeInput({ mode: 'serial', a2aEnabled: true, nativeL0Injected: true }); + assert.equal(new mod.D8Resolver().resolve(input).status, 'skipped'); + }); + + it('D9 fires with routing feedback items', () => { + const input = makeInput({ mentionRoutingItems: ['@unknown1', '@unknown2', '@unknown3'] }); + const result = new mod.D9Resolver().resolve(input); + assert.equal(result.status, 'fired'); + // Only first 2 items + assert.equal(result.vars.UNROUTED_MENTIONS, '@unknown1、@unknown2'); + }); + + it('D10 fires with critique tag', () => { + const input = makeInput({ promptTags: ['critique'] }); + assert.equal(new mod.D10Resolver().resolve(input).status, 'fired'); + }); + + it('D10 skips without critique tag', () => { + assert.equal(new mod.D10Resolver().resolve(makeInput()).status, 'skipped'); + }); +}); + +describe('Turn resolvers D11-D21, R1-R2, N1', () => { + /** @type {typeof import('../dist/domains/prompt-hooks/resolvers/turn-resolvers-b.js')} */ + let mod; + + it('load module', async () => { + mod = await import('../dist/domains/prompt-hooks/resolvers/turn-resolvers-b.js'); + }); + + it('D11 fires with skill tag', () => { + const input = makeInput({ promptTags: ['skill:tdd'] }); + const result = new mod.D11Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.SKILL_NAME, 'tdd'); + }); + + it('D12 fires with qualifying active participant', () => { + const input = makeInput({ + activeParticipants: [ + { catId: 'codex', label: '缅因猫(codex)', lastMessageAt: 1000 }, + { catId: 'opus', label: '布偶猫(opus)', lastMessageAt: 2000 }, + ], + }); + const result = new mod.D12Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.ACTIVE_LABEL, '缅因猫(codex)'); + }); + + it('D12 skips when only self is active', () => { + const input = makeInput({ + activeParticipants: [{ catId: 'opus', label: '布偶猫(opus)', lastMessageAt: 1000 }], + }); + assert.equal(new mod.D12Resolver().resolve(input).status, 'skipped'); + }); + + it('D13 fires with routing policy', () => { + const input = makeInput({ routingPolicyParts: 'review avoid @codex (recent conflict)' }); + const result = new mod.D13Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.ok(result.vars.ROUTING_PARTS.includes('avoid @codex')); + }); + + it('D14 fires with SOP hint', () => { + const input = makeInput({ + sopStageHint: { + featureId: 'F237', + stage: 'implement', + suggestedSkill: 'tdd', + suggestedSkillSource: 'mission-hub', + }, + }); + const result = new mod.D14Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.FEATURE_ID, 'F237'); + assert.ok(result.vars.SOURCE_PART.includes('mission-hub')); + }); + + it('D15 always fires — voice on', () => { + const result = new mod.D15Resolver().resolve(makeInput({ voiceMode: true })); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.TEMPLATE_VARIANT, 'D15_on'); + }); + + it('D15 always fires — voice off', () => { + const result = new mod.D15Resolver().resolve(makeInput()); + assert.equal(result.vars.TEMPLATE_VARIANT, 'D15_off'); + }); + + it('D16 fires with bootcamp state', () => { + const input = makeInput({ + bootcampState: { phase: 'explore', leadCat: 'opus', selectedTaskId: 'task-1' }, + threadId: 'thread-abc', + bootcampMemberCount: 3, + }); + const result = new mod.D16Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.PHASE, 'explore'); + assert.ok(result.vars.THREAD_PART.includes('thread-abc')); + assert.ok(result.vars.MEMBERS_PART.includes('3')); + }); + + it('D17 fires with guide lines', () => { + const input = makeInput({ guidePromptLines: '## Guide: Getting Started\nStep 1...' }); + assert.equal(new mod.D17Resolver().resolve(input).status, 'fired'); + }); + + it('D18 fires with world context', () => { + const input = makeInput({ + worldContext: { + worldName: 'Testworld', + worldStatus: 'active', + constitutionLine: 'Constitution: Be kind', + sceneName: 'Scene1', + sceneStatus: 'active', + charactersBlock: 'Characters:\n- Alice', + canonBlock: '', + recentEventsBlock: '', + careHintLine: '', + }, + }); + const result = new mod.D18Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.WORLD_NAME, 'Testworld'); + }); + + it('D19 fires with always-on docs', () => { + const input = makeInput({ alwaysOnDocsBlock: '### Doc1\n\nContent' }); + assert.equal(new mod.D19Resolver().resolve(input).status, 'fired'); + }); + + it('D20 fires with signals', () => { + const input = makeInput({ activeSignalsBlock: '### [S1] Title (HN/T1)\nContent' }); + assert.equal(new mod.D20Resolver().resolve(input).status, 'fired'); + }); + + it('D21 fires when a2a needed and returns CC_MENTION', () => { + const input = makeInput({ + mode: 'serial', + a2aEnabled: true, + nativeL0Injected: false, + }); + const result = new mod.D21Resolver().resolve(input); + assert.equal(result.status, 'fired'); + assert.equal(result.vars.CC_MENTION, '@lang'); + }); + + it('D21 skips when native L0', () => { + const input = makeInput({ mode: 'serial', a2aEnabled: true, nativeL0Injected: true }); + assert.equal(new mod.D21Resolver().resolve(input).status, 'skipped'); + }); + + it('R1, R2, N1 always fire', () => { + const input = makeInput(); + assert.equal(new mod.R1Resolver().resolve(input).status, 'fired'); + assert.equal(new mod.R2Resolver().resolve(input).status, 'fired'); + assert.equal(new mod.N1Resolver().resolve(input).status, 'fired'); + }); +}); diff --git a/packages/api/test/invoke-single-cat.test.js b/packages/api/test/invoke-single-cat.test.js index 9487247fa7..5f6ac33fb1 100644 --- a/packages/api/test/invoke-single-cat.test.js +++ b/packages/api/test/invoke-single-cat.test.js @@ -5912,6 +5912,92 @@ describe('invokeSingleCat audit events (P1 fix)', () => { await assert.rejects(readFile(seenConfigPath, 'utf-8')); }); + for (const { label, baseUrl } of [ + { label: 'GLM v4', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' }, + { label: 'GLM Coding Plan v4', baseUrl: 'https://api.z.ai/api/coding/paas/v4' }, + ]) { + it(`clowder-ai#1113: ${label} OpenCode binding preserves versioned baseUrl`, async () => { + const { createProviderProfile } = await import('./helpers/create-test-account.js'); + const root = await mkdtemp(join(tmpdir(), 'clowder1113-opencode-glm-v4-')); + process.env.CAT_CAFE_GLOBAL_CONFIG_ROOT = root; + const apiDir = join(root, 'packages', 'api'); + await mkdir(apiDir, { recursive: true }); + await writeFile(join(root, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n', 'utf-8'); + + const customProfile = await createProviderProfile(root, { + provider: 'openai', + name: 'zhipu-api', + mode: 'api_key', + authType: 'api_key', + protocol: 'openai', + baseUrl, + apiKey: 'sk-zhipu-key', + models: ['zhipu/glm-4.6v'], + setActive: false, + }); + + const registrySnapshot = catRegistry.getAllConfigs(); + const originalConfig = catRegistry.tryGet('opencode')?.config; + assert.ok(originalConfig, 'opencode config should exist in registry'); + const boundCatId = `opencode-zhipu-${label.toLowerCase().replace(/\W+/g, '-')}`; + catRegistry.register(boundCatId, { + ...originalConfig, + id: boundCatId, + mentionPatterns: [`@${boundCatId}`], + clientId: 'opencode', + accountRef: customProfile.id, + defaultModel: 'zhipu/glm-4.6v', + provider: 'zhipu', + }); + + const optionsSeen = []; + let seenConfigPath; + let seenRuntimeConfig; + const service = { + l0CompilerFn: dummyL0CompilerFn, + async *invoke(_prompt, options) { + optionsSeen.push(options ?? {}); + seenConfigPath = options?.callbackEnv?.OPENCODE_CONFIG; + assert.ok(seenConfigPath, 'GLM OpenCode binding should receive OPENCODE_CONFIG'); + seenRuntimeConfig = JSON.parse(await readFile(seenConfigPath, 'utf-8')); + yield { type: 'done', catId: 'opencode', timestamp: Date.now() }; + }, + }; + + const deps = makeDeps(); + const previousCwd = process.cwd(); + try { + process.chdir(apiDir); + const messages = await collect( + invokeSingleCat(deps, { + catId: boundCatId, + service, + prompt: 'test GLM v4 routing', + userId: 'user-clowder1113-opencode-glm-v4', + threadId: 'thread-clowder1113-opencode-glm-v4', + isLastCat: true, + }), + ); + assert.ok(messages.some((m) => m.type === 'done')); + } finally { + process.chdir(previousCwd); + catRegistry.reset(); + for (const [id, config] of Object.entries(registrySnapshot)) { + catRegistry.register(id, config); + } + await rmWithRetry(root); + } + + const callbackEnv = optionsSeen[0]?.callbackEnv ?? {}; + assert.equal(callbackEnv.CAT_CAFE_OC_API_KEY, 'sk-zhipu-key'); + assert.equal(callbackEnv.CAT_CAFE_OC_BASE_URL, baseUrl); + assert.equal(seenRuntimeConfig?.model, 'zhipu/glm-4.6v'); + assert.equal(seenRuntimeConfig?.provider?.zhipu?.npm, '@ai-sdk/openai-compatible'); + assert.deepStrictEqual(seenRuntimeConfig?.provider?.zhipu?.models, { 'glm-4.6v': { name: 'glm-4.6v' } }); + await assert.rejects(readFile(seenConfigPath, 'utf-8')); + }); + } + it('clowder-ai#223: bare model + provider assembles composite model for custom provider routing', async () => { const { createProviderProfile } = await import('./helpers/create-test-account.js'); const root = await mkdtemp(join(tmpdir(), 'f189-oc-bare-model-')); diff --git a/packages/api/test/l0-pipeline-equivalence.test.js b/packages/api/test/l0-pipeline-equivalence.test.js new file mode 100644 index 0000000000..b917cb451f --- /dev/null +++ b/packages/api/test/l0-pipeline-equivalence.test.js @@ -0,0 +1,148 @@ +/** + * F237 Phase 2 AC-P2-14a: L0 compiler ↔ pipeline L-hook equivalence. + * + * Proves that the L0 compiler's loadL0SectionTemplate() output for L1-L7 + * matches the pipeline's L-hook patch content (whitespace-normalized). + * + * When this test passes, the L0 compiler can safely switch to consuming + * pipeline-produced L-hook content instead of reading template files directly. + */ + +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { after, before, describe, it } from 'node:test'; + +/** Normalize whitespace for comparison. */ +function normalize(s) { + return s.replace(/\s+/g, ' ').trim(); +} + +/** Strip compiler-only annotation lines (same as L0 compiler's logic). */ +function stripAnnotations(raw) { + const SEGMENT_LABEL = /^── \[[A-Z]\d+] .+──$/; + return raw + .split('\n') + .filter((line) => { + const trimmed = line.trim(); + return !trimmed.startsWith('\n\n`, + 'utf-8', + ); + } + + created++; +} + +console.log(`Generated ${created} hook directories, skipped ${skipped} existing.`); +console.log(`Total: ${TIER1_HOOKS.length} Tier 1 hooks.`); +console.log(`Session-init: ${TIER1_HOOKS.filter((h) => h.stage === 'session-init').length}`); +console.log(`Per-turn: ${TIER1_HOOKS.filter((h) => h.stage === 'per-turn').length}`); + +function slugify(name) { + return name + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 40); +}