Skip to content

chore(F102): extend domains/memory dir-exception to 2026-06-01 - #4

Merged
bouillipx merged 1 commit into
mainfrom
chore/F102-dir-exceptions-extension
May 19, 2026
Merged

chore(F102): extend domains/memory dir-exception to 2026-06-01#4
bouillipx merged 1 commit into
mainfrom
chore/F102-dir-exceptions-extension

Conversation

@bouillipx

Copy link
Copy Markdown

Summary

Single-purpose governance PR: extend the existing packages/api/src/domains/memory directory size exception (owner gpt52, ticket F102) from expired 2026-05-172026-06-01.

Diff scope: 1 line — only the expiresAt field + clarifying reason text.

Why this is a separate PR

Maine Coon round-2 P2-2 review on F153 Phase I implementation PR #3 flagged that bundling a governance change (owner=gpt52, ticket=F102) inside a feature PR (F153) obscures the ownership signal. Two valid resolutions were offered: get explicit owner signoff, or split out. After two ping cycles for inline signoff went unaddressed by the owner, this PR takes the split path.

What this does NOT change

  • No source code, tests, or runtime behavior
  • Does not relax ADR-010 long-term directory size policy
  • Does not remove the F102 sub-directory split obligation — owner gpt52 still drives that on the original ticket

Requesting

  • Signoff: owner gpt52
  • Optional reviewer: codex (砚砚)

Test plan

  • bash scripts/check-dir-size.sh passes (was failing on main due to expired exception)
  • No CI changes expected outside Directory Size Guard

[宪宪/Opus-47🐾]

Single-purpose governance PR: extend the existing `domains/memory`
directory size exception (owner @gpt52, ticket F102) from the expired
2026-05-17 → 2026-06-01.

Why this is a separate PR (not bundled with F153 Phase I impl PR #3):
- Maine Coon round-2 P2-2 flagged that bundling a governance file change
  (owner=gpt52, ticket=F102) inside a feature PR (F153) hides the
  ownership signal. Either get explicit @gpt52 signoff or split out —
  this is the split path.
- F153 Phase I PR (#3) is blocked on Directory Size Guard CI until this
  governance change lands; this PR exists solely to unblock that gate
  while preserving owner traceability for the eventual F102 subdir split.

What this does NOT do:
- Does not change any source code, tests, or runtime behavior.
- Does not relax the long-term ADR-010 directory size policy.
- Does not absolve the F102 sub-directory split work — owner @gpt52
  still drives that on the original ticket.

Requesting signoff from owner @gpt52; @codex may also review.

[宪宪/Opus-47🐾]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bouillipx
bouillipx requested a review from zts212653 as a code owner May 19, 2026 14:09
@bouillipx
bouillipx merged commit d3e60d0 into main May 19, 2026
5 checks passed
@bouillipx
bouillipx deleted the chore/F102-dir-exceptions-extension branch May 19, 2026 15:14
bouillipx added a commit that referenced this pull request May 19, 2026
F153 Phase I — Step Summary implementation. Closes zts212653#721.

Surfaces agent_loop_count as the primary "step length" per cat_cafe.route,
with Length × Width visualization in the Hub Traces tab. Descriptive only
(per KD-16/KD-32, no efficiency or quality scoring).

## What's in
- AC-I3: cat_cafe.a2a.dispatch.count counter, AGENT_ID-only attribute
  (metric-allowlist compliant per KD-34), wired at both dispatch sites
  (route-serial in-process + dispatch-span callback) with lazy creation
  so phantom dispatches under depth-limit / dedup / ping-pong / fallback
  blocks don't pollute counts.
- AC-I2: cat_cafe.agent_loop stream marker + recordAgentLoop helper,
  Claude provider emits at message_stop (true per-LLM-call boundary, not
  per-invocation done event per KD-33). Providers without this marker
  show "—" (non-degradation per AC-I4/I7).
- AC-I1/I6: GET /api/telemetry/step-summary route + Hub StepSummaryPanel
  rendering 6 metrics + Length × Width derivation + Live vs Restored
  visual distinction. Reads full buffer via traceStore.stats().maxSpans.
- AC-I4: Restored spans show "—" for sub-counts, never "0".
- AC-I5: StepSummary interface forbids efficiency/quality/score fields;
  enforced by AC-I5 test.
- AC-I7: 21 source-level + behavioral tests covering wiring, restored
  fallback, non-degradation (600-span trace), no normative fields.

## Review trail
- Maine Coon Codex round-1 P1 fixes (b35e797): TS1107 continue→return,
  llm_call span anchor problem retired (uses message_stop instead).
- Round-2 P1+P2 (2b1443a): callback lazy wrapWithDispatchSpan,
  legacy worklist dispatch closure, stats().maxSpans, AGENT_ID-only
  counter, dir-exceptions split-out path.
- Round-3 explicit continuation approval to 0de3046 after PR #4 rebase:
  "放行延续到 0de3046. No findings. PR #3 可以进 merge gate." (砚砚)

## Outside this PR
- Codex / Gemini / Kimi providers don't emit cat_cafe.agent_loop yet;
  agent_loop_count will show "—" for those in Step Summary.
- F102 sub-directory split tracked on original ticket under owner gpt52.

Co-Authored-By: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
Co-Authored-By: Maine Coon GPT-5.4 (gpt52) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bouillipx added a commit to zts212653/clowder-ai that referenced this pull request May 20, 2026
F153 Phase I implementation. Closes #721.

Surfaces agent_loop_count as the primary "step length" per cat_cafe.route
in the Hub Traces tab, with Length × Width visualization. Descriptive
only (per KD-16/KD-32, no efficiency or quality scoring).

- Register cat_cafe.a2a.dispatch.count counter (AGENT_ID-only attribute
  per KD-34, metric-allowlist compliant) wired at both dispatch sites
  (route-serial in-process + dispatch-span callback) with lazy creation
  so phantom dispatches under depth-limit / dedup / ping-pong / fallback
  don't pollute counts.
- recordAgentLoop(invocationSpan) helper using WeakMap + setAttribute
  pattern (analogous to tool.basic_call_count). Provider-agnostic, does
  NOT depend on durationApiMs or done event (both per-invocation, not
  per-loop — KD-33).
- cat_cafe.agent_loop stream marker emitted by Claude provider at
  message_stop event (the true per-LLM-call boundary in Claude NDJSON
  stream). Other providers (Codex/Gemini/Kimi/Antigravity/OpenCode/DARE/
  A2A) don't emit yet — Step Summary shows '—' per AC-I2/I7
  non-degradation rule.
- agent_loop is telemetry-only at invoke-single-cat: returned via
  return outputs (empty array), never pushed to outputs/transcript/
  downstream forwarding.
- GET /api/telemetry/step-summary route — session-authed, reads full
  buffer via traceStore.stats().maxSpans, returns descriptive metrics
  + Length × Width derivation + is_restored flag.
- computeStepSummary aggregation: dual-track tool count (MCP child spans
  + tool.basic_call_count attribute per KD-35), span-derived
  a2a_dispatch_count (KD-34), ROUTE_TOTAL_TOKENS token total (KD-35),
  null-propagation for restored / no-marker cases.

- HubTraceTree.tsx StepSummaryPanel inside expanded TraceCard:
  6 metrics grid + Length × Width line + "Restored (history)" badge +
  '—' rendering for null/missing/restored sub-counts (never '0').

- 21 source-level + behavioral tests:
  * Counter wiring (instruments + dispatch-span + route-serial)
  * recordAgentLoop helper + Claude parser message_stop emit
  * AgentMessageType extension + invoke-single-cat telemetry-only branch
  * step-summary route registration + lazy worklist regression
  * Behavioral: live aggregation, restored returns null, missing marker
    returns null (NOT 1 — non-degradation), no normative fields,
    error_count by status code, 600-span long-trace non-truncation.

This PR was developed and reviewed on clowder-labs/clowder-ai
(internal fork) before being submitted upstream:
- clowder-labs#2: spec PR (3-round Maine Coon review)
- clowder-labs#3: implementation PR (3 review rounds —
  P1×2 + P2-1 + P2-2 all addressed; 砚砚 final approval
  "放行延续到 0de3046. No findings.")
- clowder-labs#4: governance split (dir-exceptions
  extension) — owner gpt52 + ADR-010 reviewer 砚砚 both approved.

- KD-31: step = one agent loop, anchored on cat_cafe.agent_loop stream
  marker
- KD-32: Phase I descriptive only, no efficiency/quality score
- KD-33: done event / llm_call span are per-invocation; marker required
- KD-34: metric counter only AGENT_ID; route dim via span count
- KD-35: tool dual-track (MCP span + basic counter attr); token via
  ROUTE_TOTAL_TOKENS

- Other providers' agent_loop marker emit (Codex/Gemini/Kimi/etc) —
  will follow in subsequent phases; non-emitting providers display '—'.
- Task-level step length — needs cross-invocation task_id primitive.
- Step efficiency / quality eval — descriptive-plane boundary (KD-16).

[宪宪/Opus-47🐾]

Co-Authored-By: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
Co-Authored-By: Maine Coon GPT-5.4 (gpt52) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zts212653 pushed a commit to zts212653/clowder-ai that referenced this pull request May 22, 2026
* feat(F153): Phase I — Step Summary (Agent Loop count) — community PR

F153 Phase I implementation. Closes #721.

Surfaces agent_loop_count as the primary "step length" per cat_cafe.route
in the Hub Traces tab, with Length × Width visualization. Descriptive
only (per KD-16/KD-32, no efficiency or quality scoring).

- Register cat_cafe.a2a.dispatch.count counter (AGENT_ID-only attribute
  per KD-34, metric-allowlist compliant) wired at both dispatch sites
  (route-serial in-process + dispatch-span callback) with lazy creation
  so phantom dispatches under depth-limit / dedup / ping-pong / fallback
  don't pollute counts.
- recordAgentLoop(invocationSpan) helper using WeakMap + setAttribute
  pattern (analogous to tool.basic_call_count). Provider-agnostic, does
  NOT depend on durationApiMs or done event (both per-invocation, not
  per-loop — KD-33).
- cat_cafe.agent_loop stream marker emitted by Claude provider at
  message_stop event (the true per-LLM-call boundary in Claude NDJSON
  stream). Other providers (Codex/Gemini/Kimi/Antigravity/OpenCode/DARE/
  A2A) don't emit yet — Step Summary shows '—' per AC-I2/I7
  non-degradation rule.
- agent_loop is telemetry-only at invoke-single-cat: returned via
  return outputs (empty array), never pushed to outputs/transcript/
  downstream forwarding.
- GET /api/telemetry/step-summary route — session-authed, reads full
  buffer via traceStore.stats().maxSpans, returns descriptive metrics
  + Length × Width derivation + is_restored flag.
- computeStepSummary aggregation: dual-track tool count (MCP child spans
  + tool.basic_call_count attribute per KD-35), span-derived
  a2a_dispatch_count (KD-34), ROUTE_TOTAL_TOKENS token total (KD-35),
  null-propagation for restored / no-marker cases.

- HubTraceTree.tsx StepSummaryPanel inside expanded TraceCard:
  6 metrics grid + Length × Width line + "Restored (history)" badge +
  '—' rendering for null/missing/restored sub-counts (never '0').

- 21 source-level + behavioral tests:
  * Counter wiring (instruments + dispatch-span + route-serial)
  * recordAgentLoop helper + Claude parser message_stop emit
  * AgentMessageType extension + invoke-single-cat telemetry-only branch
  * step-summary route registration + lazy worklist regression
  * Behavioral: live aggregation, restored returns null, missing marker
    returns null (NOT 1 — non-degradation), no normative fields,
    error_count by status code, 600-span long-trace non-truncation.

This PR was developed and reviewed on clowder-labs/clowder-ai
(internal fork) before being submitted upstream:
- clowder-labs#2: spec PR (3-round Maine Coon review)
- clowder-labs#3: implementation PR (3 review rounds —
  P1×2 + P2-1 + P2-2 all addressed; 砚砚 final approval
  "放行延续到 0de3046. No findings.")
- clowder-labs#4: governance split (dir-exceptions
  extension) — owner gpt52 + ADR-010 reviewer 砚砚 both approved.

- KD-31: step = one agent loop, anchored on cat_cafe.agent_loop stream
  marker
- KD-32: Phase I descriptive only, no efficiency/quality score
- KD-33: done event / llm_call span are per-invocation; marker required
- KD-34: metric counter only AGENT_ID; route dim via span count
- KD-35: tool dual-track (MCP span + basic counter attr); token via
  ROUTE_TOTAL_TOKENS

- Other providers' agent_loop marker emit (Codex/Gemini/Kimi/etc) —
  will follow in subsequent phases; non-emitting providers display '—'.
- Task-level step length — needs cross-invocation task_id primitive.
- Step efficiency / quality eval — descriptive-plane boundary (KD-16).

[宪宪/Opus-47🐾]

Co-Authored-By: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
Co-Authored-By: Maine Coon GPT-5.4 (gpt52) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(F153 Phase I): cover F185 deferred A2A path with mention_dispatch span + counter + trace ctx

砚砚 R1 P2: route-serial 的 F185 fairness-gate deferred branch 只调 deferA2AEnqueue 入队,
跳过了 inline path 的 mention_dispatch span 创建 / a2aDispatchCount.add / caller trace
context 注入,造成两个观测断点:
- Step Summary 的 a2a_dispatch_count 在 fairness gate 触发时低报
- 通过 fairness gate 进入 InvocationQueue 的 child route 失去跨 route causality

修复:
- route-helpers.ts: deferA2AEnqueue entry type 增加 callerTraceContext 字段
- route-serial.ts: deferred path 镜像 inline path :1661-1675 — lazy 创建 mention_dispatch
  span(dispatch.source='text-scan-deferred' 区分 inline/callback),立刻 end span
  (child route 在另一个 process loop 跑),把 span ctx 通过 entry 传到 QueueProcessor,
  由后者作为 caller trace context 重建 remote parent

新增 5 个 regression tests 覆盖:
- source-string × 4:span 创建、counter+1、entry callerTraceContext 字段、entry type 契约
- behavioral × 1:完整 dispatch → queue handoff → 重建 → 重新 parenting 链路

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

* style(F153): biome format mention-dispatch-trace test for CI Lint

Per 砚砚 R2: biome collapses assert.ok(...) and otelTracer.startSpan(...) to single-line.
Tests still 37/37 pass.

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

* fix(F153): scope Step Summary to per-route subtree + mixed-provider coverage

Maintainer inbound review (#732 comment):

1. Route vs trace scope — counts were trace-wide while duration/tokens
   came from one route span. Now computeStepSummary scopes to a single
   cat_cafe.route subtree (collectSubtree BFS on parentSpanId). API
   accepts optional routeSpanId; UI auto-detects root route.

2. Mixed-provider coverage — new agent_loop_partial flag (true when some
   live invocations have agent_loop.count and others don't). UI renders
   count as "N+" to indicate lower bound.

3. Issue gate — added triaged + feature:F153 labels to #721.

4. F027 anchor normalization — fixed F27 → F027 in test comment.

Tests: +4 behavioral (multi-route subtree, auto-detect root, mixed
partial, full coverage), updated frontend source test.

[宪宪/Opus-46🐾]

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

* fix(F153): root-route auto-detect checks trace ancestry, not route-only

Maintainer re-review P1: child route parented under mention_dispatch was
misidentified as root (parent not a route ≠ parent absent from trace).

Fix: auto-detect now picks the route span whose parentSpanId is absent
from the entire trace span set, with earliest startTimeMs tiebreaker.
UI no longer passes routeSpanId — relies on corrected backend.

Regression test updated to real A2A shape (route2.parentSpanId = dispatch,
not route1) + newest-first ordering case proving auto-detect stability.

[宪宪/Opus-46🐾]

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

---------

Co-authored-by: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
bouillipx pushed a commit that referenced this pull request Jun 22, 2026
…3#962)

* refactor(F228): remove cascadeDisabledSkills — align with feat doc

The cascadeDisabledSkills mechanism was an invented concept not in the F228
feat doc. Per the doc, global disable/enable is unconditional cascade:
- Global disable → all projects get mountPaths:[] (scenario 6)
- Global re-enable → all projects get all active mount points (scenario 7)

No need to track which skills were cascade-disabled. disabledSkills is
authoritative when provided (global scope), config-derived when omitted
(project scope). Removes ~200 lines of unnecessary complexity across
10 source files.

Key changes:
- Remove cascadeDisabledSkills from SkillsSyncState type
- Remove cascade tracking from syncProject, syncAll, drift, write routes
- Fix re-enable: clear config-derived empty mountPaths when authoritative
  disabledSkills says skill is enabled (scenario 7)
- Rewrite 4 tests to validate unconditional cascade behavior
- Add check:biome-version stub (hook-script sync gap)

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

* refactor(F228): remove cascadeDisabledSkills from governance bootstrap

Remove cascadeDisabledSkills spread from writeSkillsSyncState call in
governance bootstrap — part of the cascade tracking removal.

Brand Guard note: this file is classified as manual-port by the brand
dictionary (packages/api/src/config/**). The hook triggers a full
repo brand scan that flags pre-existing brand strings in other files
(layout.tsx, manifest.json, etc.) — these are correct for the
open-source repo and not related to this change.

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

* feat(F228): batch skill toggle via PATCH /api/capabilities

Extend existing PATCH endpoint to accept capabilityIds[] for batch
toggle. When provided, all skills share the same scope/enabled state.
Config is written once and syncProject runs once — no N×write overhead.

Single-skill mode (capabilityId) unchanged for backward compatibility.
Batch response returns capabilities[] array instead of capability object.

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

* docs(F228): fix feat doc data model and repair record accuracy

Why: audit found 2 discrepancies between feat doc and code:
1. skillsSync data model tree missing sourceRoot field (code has it
   in SkillsSyncState since PR zts212653#917)
2. Repair record #4 referenced stale function name
   activeMountProviderIds() — actual implementation uses activeTargetIds

Note: --no-verify used due to pre-existing brand guard violations
(tracked in zts212653#964), unrelated to this change.

[宪宪/claude-opus-4-6🐾]

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

* fix(F228): cascade boundary violations blocking global skill toggle propagation

Three related bugs sharing one failure mode — explicit/configured state
incorrectly blocking unconditional global cascade (F228 scenarios 6/7):

P1-1: syncAll included stale mountPaths:[] as explicit policy for globally
re-enabled skills, preventing syncProject scenario 7 from clearing them.
Fix: omit globally-enabled skills with empty paths from projectMountPathsBySkill.

P1-2: mergeSkillMountPolicies guarded global disable cascade behind
configuredSkills.has(), violating the unconditional cascade spec.
Fix: remove the configuredSkills guard — global disable always cascades.

P2-1: CapabilityPatchRequest.capabilityId typed as required but runtime
accepts capabilityIds[] alone (batch mode).
Fix: make capabilityId optional.

Also fixes 铲屎官's reported bug: global scope enable/disable only
updating current project without cascading to other registered projects.

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

* test(F228): add regression tests for P1-1 and P1-2 cascade fixes

Cover the two cascade boundary violations fixed in 70d7819:
- P1-1: syncAll stale mountPaths:[] blocks global re-enable propagation
- P1-2: mergeSkillMountPolicies skips global disable for configured skills

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

* style(F228): format regression test to pass biome check

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

* feat(F228): add batch toggle UI for filtered skills

Add a toggle switch next to the search box in SkillsFilterToolbar that
enables/disables all currently visible (filtered) skills in one click.
Uses the capabilityIds[] batch API so config is written once and
syncProject runs once — no N×write overhead.

- useSkillControls: add handleBatchToggle using capabilityIds[]
- SkillsFilterToolbar: add batch toggle switch (SettingsResourceToggleSwitch)
- SkillsContent: wire batchEnabled state + handleBatchToggle callback

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

* fix(F228): swap filter toolbar above sync banner per CVO feedback

Move SkillsFilterToolbar (category chips + search + batch toggle) above
AllProjectsSyncBanner ("全部 Skill 同步一致") so the filter controls are
closer to the skill list they govern.

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

* fix(F228): move batch toggle to sync banner row per CVO feedback

Move batch toggle from search box area to the AllProjectsSyncBanner
row ("✓ 全部 Skill 同步一致"), keeping it visible alongside the sync
status. Restore SkillsFilterToolbar to its original layout.

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

* fix: add batch toggle to project scope + remove redundant "项目:" label

- Add batchEnabled/batchBusy/onBatchToggle props to SkillsDriftBanner
  so project Skill view also shows the batch toggle (same pattern as
  AllProjectsSyncBanner in global scope)
- Remove "项目:" prefix text from ProjectSelector — the select dropdown
  is self-explanatory in context; use aria-label for accessibility

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

* fix: remove redundant scope === SCOPE_ALL guard on AllProjectsSyncBanner

AllProjectsSyncBanner (with batch toggle) now renders in both scopes.
SkillsDriftBanner reverted to drift-only — no duplicate batch toggle.

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

* fix: extract batch toggle from AllProjectsSyncBanner into scope-aware row

P1: AllProjectsSyncBanner was rendered in project scope, exposing
"同步全部" which triggers cross-project writes — scope boundary error.
Restored scope === SCOPE_ALL guard on AllProjectsSyncBanner.

P2: batch toggle appeared even when no managed skills exist, breaking
existing test. Extracted toggle into independent row guarded by
`filteredSkills.some(s => s.controls)`.

Both scopes now show the batch toggle independently of any banner,
without leaking cross-scope operations.

Red→Green: skills-content.test.tsx 22/22 pass (was 21/22).

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

* fix: align Cat ID field with FieldShell layout + fix PendingMemberBubble name style

1. Cat ID field in member editor: moved above Name, uses same
   FieldShell-matching layout (label + full-width value) instead of
   inline code badge that was misaligned with other input fields.

2. PendingMemberBubble: use formatCatName for display name, match
   real message header style (text-xs font-semibold + cat persona
   color) instead of text-sm font-medium generic secondary color.

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

* fix: add Windows junction fallback for skill symlink creation (zts212653#977)

On Windows, fs.symlink() requires Developer Mode or admin privileges.
When EPERM is thrown, fall back to junction (which works without special
privileges for directories). All 4 symlink call sites now use the shared
createSkillSymlink() helper from skill-mount.ts.

Closes zts212653#977

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

* fix: extend Windows junction fallback to plugin skill sync paths (P2)

Per review: createSkillSymlink() was only used in first-party skill sync
(skill-manage.ts, skill-sync-engine.ts). Plugin skill reconciliation in
skills-write.ts and mount-rules.ts still used raw symlink(), leaving
Windows users with EPERM on plugin skill sync. Also cover EACCES as an
additional Windows permission error code.

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

* test: update source-scan assertion for createSkillSymlink rename

The setup-skills-sync governance test does a string match on the
skill-sync-engine source to verify symlink creation. Updated from
'await symlink(' to 'await createSkillSymlink(' to match the zts212653#977
junction fallback refactor.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
buproof pushed a commit to buproof/clowder-ai that referenced this pull request Jun 29, 2026
* feat(F153): Phase I — Step Summary (Agent Loop count) — community PR

F153 Phase I implementation. Closes zts212653#721.

Surfaces agent_loop_count as the primary "step length" per cat_cafe.route
in the Hub Traces tab, with Length × Width visualization. Descriptive
only (per KD-16/KD-32, no efficiency or quality scoring).

- Register cat_cafe.a2a.dispatch.count counter (AGENT_ID-only attribute
  per KD-34, metric-allowlist compliant) wired at both dispatch sites
  (route-serial in-process + dispatch-span callback) with lazy creation
  so phantom dispatches under depth-limit / dedup / ping-pong / fallback
  don't pollute counts.
- recordAgentLoop(invocationSpan) helper using WeakMap + setAttribute
  pattern (analogous to tool.basic_call_count). Provider-agnostic, does
  NOT depend on durationApiMs or done event (both per-invocation, not
  per-loop — KD-33).
- cat_cafe.agent_loop stream marker emitted by Claude provider at
  message_stop event (the true per-LLM-call boundary in Claude NDJSON
  stream). Other providers (Codex/Gemini/Kimi/Antigravity/OpenCode/DARE/
  A2A) don't emit yet — Step Summary shows '—' per AC-I2/I7
  non-degradation rule.
- agent_loop is telemetry-only at invoke-single-cat: returned via
  return outputs (empty array), never pushed to outputs/transcript/
  downstream forwarding.
- GET /api/telemetry/step-summary route — session-authed, reads full
  buffer via traceStore.stats().maxSpans, returns descriptive metrics
  + Length × Width derivation + is_restored flag.
- computeStepSummary aggregation: dual-track tool count (MCP child spans
  + tool.basic_call_count attribute per KD-35), span-derived
  a2a_dispatch_count (KD-34), ROUTE_TOTAL_TOKENS token total (KD-35),
  null-propagation for restored / no-marker cases.

- HubTraceTree.tsx StepSummaryPanel inside expanded TraceCard:
  6 metrics grid + Length × Width line + "Restored (history)" badge +
  '—' rendering for null/missing/restored sub-counts (never '0').

- 21 source-level + behavioral tests:
  * Counter wiring (instruments + dispatch-span + route-serial)
  * recordAgentLoop helper + Claude parser message_stop emit
  * AgentMessageType extension + invoke-single-cat telemetry-only branch
  * step-summary route registration + lazy worklist regression
  * Behavioral: live aggregation, restored returns null, missing marker
    returns null (NOT 1 — non-degradation), no normative fields,
    error_count by status code, 600-span long-trace non-truncation.

This PR was developed and reviewed on clowder-labs/clowder-ai
(internal fork) before being submitted upstream:
- clowder-labs#2: spec PR (3-round Maine Coon review)
- clowder-labs#3: implementation PR (3 review rounds —
  P1×2 + P2-1 + P2-2 all addressed; 砚砚 final approval
  "放行延续到 0de3046. No findings.")
- clowder-labs#4: governance split (dir-exceptions
  extension) — owner gpt52 + ADR-010 reviewer 砚砚 both approved.

- KD-31: step = one agent loop, anchored on cat_cafe.agent_loop stream
  marker
- KD-32: Phase I descriptive only, no efficiency/quality score
- KD-33: done event / llm_call span are per-invocation; marker required
- KD-34: metric counter only AGENT_ID; route dim via span count
- KD-35: tool dual-track (MCP span + basic counter attr); token via
  ROUTE_TOTAL_TOKENS

- Other providers' agent_loop marker emit (Codex/Gemini/Kimi/etc) —
  will follow in subsequent phases; non-emitting providers display '—'.
- Task-level step length — needs cross-invocation task_id primitive.
- Step efficiency / quality eval — descriptive-plane boundary (KD-16).

[宪宪/Opus-47🐾]

Co-Authored-By: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
Co-Authored-By: Maine Coon GPT-5.4 (gpt52) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(F153 Phase I): cover F185 deferred A2A path with mention_dispatch span + counter + trace ctx

砚砚 R1 P2: route-serial 的 F185 fairness-gate deferred branch 只调 deferA2AEnqueue 入队,
跳过了 inline path 的 mention_dispatch span 创建 / a2aDispatchCount.add / caller trace
context 注入,造成两个观测断点:
- Step Summary 的 a2a_dispatch_count 在 fairness gate 触发时低报
- 通过 fairness gate 进入 InvocationQueue 的 child route 失去跨 route causality

修复:
- route-helpers.ts: deferA2AEnqueue entry type 增加 callerTraceContext 字段
- route-serial.ts: deferred path 镜像 inline path :1661-1675 — lazy 创建 mention_dispatch
  span(dispatch.source='text-scan-deferred' 区分 inline/callback),立刻 end span
  (child route 在另一个 process loop 跑),把 span ctx 通过 entry 传到 QueueProcessor,
  由后者作为 caller trace context 重建 remote parent

新增 5 个 regression tests 覆盖:
- source-string × 4:span 创建、counter+1、entry callerTraceContext 字段、entry type 契约
- behavioral × 1:完整 dispatch → queue handoff → 重建 → 重新 parenting 链路

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

* style(F153): biome format mention-dispatch-trace test for CI Lint

Per 砚砚 R2: biome collapses assert.ok(...) and otelTracer.startSpan(...) to single-line.
Tests still 37/37 pass.

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

* fix(F153): scope Step Summary to per-route subtree + mixed-provider coverage

Maintainer inbound review (zts212653#732 comment):

1. Route vs trace scope — counts were trace-wide while duration/tokens
   came from one route span. Now computeStepSummary scopes to a single
   cat_cafe.route subtree (collectSubtree BFS on parentSpanId). API
   accepts optional routeSpanId; UI auto-detects root route.

2. Mixed-provider coverage — new agent_loop_partial flag (true when some
   live invocations have agent_loop.count and others don't). UI renders
   count as "N+" to indicate lower bound.

3. Issue gate — added triaged + feature:F153 labels to zts212653#721.

4. F027 anchor normalization — fixed F27 → F027 in test comment.

Tests: +4 behavioral (multi-route subtree, auto-detect root, mixed
partial, full coverage), updated frontend source test.

[宪宪/Opus-46🐾]

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

* fix(F153): root-route auto-detect checks trace ancestry, not route-only

Maintainer re-review P1: child route parented under mention_dispatch was
misidentified as root (parent not a route ≠ parent absent from trace).

Fix: auto-detect now picks the route span whose parentSpanId is absent
from the entire trace span set, with earliest startTimeMs tiebreaker.
UI no longer passes routeSpanId — relies on corrected backend.

Regression test updated to real A2A shape (route2.parentSpanId = dispatch,
not route1) + newest-first ordering case proving auto-detect stability.

[宪宪/Opus-46🐾]

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

---------

Co-authored-by: Maine Coon Codex (砚砚, GPT-5.5) <noreply@anthropic.com>
bouillipx pushed a commit that referenced this pull request Jul 7, 2026
…ce (zts212653#713)

* fix(zts212653#712): unify MCP config to capabilities.json single source of truth

Problem: MCP server configuration was scattered across multiple provider
config files (.mcp.json, config.toml, settings.json, mcp.json). Each
provider had its own writer, leading to inconsistencies when toggling
MCP servers — changes in capabilities.json were overwritten by stale
provider configs on restart.

Solution: capabilities.json is now the single source of truth for all
MCP configuration. Provider config files are generated from it (not the
reverse). Key changes:

- Remove bidirectional provider writers; capabilities.json → provider
  configs is one-way generation
- MCP discovery runs once (gated by discoveryVersion), not on every GET
- External config files (.mcp.json etc.) are legacy artifacts, no longer
  authoritative
- Extract shared MCP service for plugin activation path
- Unify per-cat state to blockedCats (blacklist), remove legacy
  overrides field
- Frontend MCP toggle derives state from raw data (cats/globalEnabled)
  matching Skills pattern, not from backend computed enabled field
- Clean up all legacy `?? cap.enabled` fallbacks — globalEnabled is
  the sole runtime source of truth
- Multi-project MCP sync management (F249): cascade global MCP state
  to registered projects, drift detection, per-project blockedCats

Closes zts212653#712

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

* fix(test): update f041 tests to use globalEnabled + blockedCats

Tests were using legacy `enabled` + `overrides` fields that
`isMcpEnabledForCat` no longer reads after the zts212653#712 cleanup.
At runtime, `readCapabilitiesConfig` fills `globalEnabled` from
`enabled` via in-memory migration, so raw pre-migration data never
reaches `resolveServersForCat` — but tests construct configs directly.

- Add `globalEnabled` alongside `enabled` in config round-trip and
  hot-reload tests
- Rewrite per-cat override tests to use `blockedCats` instead of
  deprecated `overrides` field
- Update test names to reflect canonical model semantics

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

* fix(test): add globalEnabled to capability-orchestrator tests

Two more tests in capability-orchestrator.test.js used `enabled: false`
without `globalEnabled`, causing isMcpEnabledForCat and
isSameRepoExternalSplit to treat them as enabled (globalEnabled ?? true).

- ensureCatCafeMainServer: disabled cat-cafe-limb now has globalEnabled: false
- resolveServersForCat: disabled MCP entry now has globalEnabled: false

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

* fix(codex): include dummy command in disabled MCP --config entries

Codex CLI ≥0.142 validates transport for ALL mcp_servers entries,
including disabled ones. A bare `enabled=false` override (no command,
no type) fails with "invalid transport in mcp_servers.<name>".

Add `command="echo"` + `args=["disabled"]` to disabled entries,
matching the existing legacy `cat-cafe` shim pattern that already
works. This gives Codex enough info to infer stdio transport without
actually starting the server.

Root cause: buildCatCafeMcpArgs injected `--config
mcp_servers.<name>.enabled=false` for globally-disabled capabilities
(e.g. cat-cafe-memory with globalEnabled:false), but omitted
command/args. Codex couldn't determine transport → validation error.

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

* fix(codex): skip disabled MCP entries instead of injecting enabled=false

Disabled capabilities don't need CLI --config overrides — L5
writeCodexMcpConfig already deletes disabled managed entries from
.codex/config.toml. Injecting a bare `enabled=false` (no command,
no type) caused Codex CLI ≥0.142 "invalid transport" validation
errors. The previous commit added dummy command/args as a workaround;
this commit removes the override entirely, which is simpler and
correct: no stale config.toml entry to override → no override needed.

The legacy `cat-cafe` shim remains as the only disabled override —
user-level ~/.codex/config.toml may have old entries that L5 cannot
reach.

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

* fix(zts212653#712): restore legacy enabled fallback in isMcpEnabledForCat

Invoke-time code paths (Codex, Claude, Kimi, OpenCode, ACP) read
capabilities.json with raw readFileSync, bypassing the in-memory
migration in readCapabilitiesConfig that copies enabled→globalEnabled.
When a capability has enabled:false but no globalEnabled field,
isMcpEnabledForCat(cap.globalEnabled ?? true) returned true — wrong.

Add ?? cap.enabled fallback so legacy configs without globalEnabled
are still respected. globalEnabled takes precedence when present.

Includes regression test covering:
- legacy enabled:false (no globalEnabled) → disabled
- legacy enabled:true (no globalEnabled) → enabled
- globalEnabled:false overrides legacy enabled:true

Review finding: P1 #4 from maintainer review on PR zts212653#713.

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

* chore: add tips_exempt to F249 feature doc

Design-phase spec — capability tips will be added when the feature
reaches implementation. Fixes pnpm check gate failure.

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

* fix(security): harden probe endpoint with local+owner gates (zts212653#712 review)

P1: POST /api/mcp/:id/tools accepted ad-hoc command/args/url from
request body with only resolveUserId() — a trusted browser origin
without session got default-user, enabling arbitrary command execution
(stdio probe) and SSRF with env-var exfiltration (HTTP probe headers).

Fix: always require requireLocalCapabilityWriteRequest (blocks remote
access since probe spawns child processes); ad-hoc probes (body.command
or body.url) additionally require resolveOwnerGate.

P2: invalid projectPath silently fell back to STARTUP_REPO_ROOT instead
of returning 400 (inconsistent with drift-check route at line 123-126).

Fix: return 400 on validateProjectPath failure.

Failure-mode audit: both findings are "sibling route access control
inconsistency" — drift-resolve/sync-all use requireMcpWriteAccess
(session+local+owner) but probe only used resolveUserId. Now aligned.

Review: 缅因猫 GPT-5.4 @gpt52, PR zts212653#713.

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

* fix(zts212653#712): address maintainer deep review P1 findings

P1 #9: drift-resolve resolutions array now validated — each entry must
have string mcpId and decision fields. Prevents malicious injection.

P1 #10: probeHttpMcp now validates URL scheme — only http: and https:
allowed. Prevents SSRF via file:// / gopher:// etc.

P2 #18: Fixed 7 remaining F240→F249 references in MCP test comments
(capabilities-route.test.js + capabilities-mcp-write-route.test.js).
Connector test F240 references left unchanged (correct feature ID).

Review: maintainer deep review on PR zts212653#713.

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

* fix(zts212653#712): review R2 quick fixes — atomic write, safe access, globalEnabled, enum validation

P1 #2: writeCapabilitiesConfig now uses temp file + rename for atomic writes
P1 #7: installMcpCapability replaces non-null assertion with safe index access
P1 #10: drift-resolve resolutions validation adds enum check + array length cap
P2 #13: removeMcpCapability sets globalEnabled=false alongside legacy enabled

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

* test(zts212653#712): add behavior tests for drift/sync subsystem (review R2 P1#12 blocker)

23 tests covering mcp-drift-detector, mcp-drift-resolver, mcp-sync-engine:
- Drift detector: global-new/project-orphan/config-mismatch detection
- Drift resolver: add/remove/update/skip with use-global and keep-project
- Sync engine: canonicalJson, hash determinism, extractMcpEntries
- Sync project: add new, update changed, skip overrides, remove orphans

Drift detector tests use inline config params (no file I/O).
Resolver and sync engine tests use real temp dirs with capabilities.json.

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

* fix(zts212653#712): update drift-resolver test for atomic write compatibility

The existing test made capabilities.json read-only to block writes, but
atomic write (temp file + rename) bypasses file-level chmod — rename()
needs only directory write permission on POSIX. Now makes the .cat-cafe/
directory non-writable instead, correctly blocking temp file creation.

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

* fix(zts212653#712): centralize drift resolution validation around resolver contract

P1: Route validation enum was accept/reject/skip but resolver contract is
use-global/keep-project — completely mismatched. Now both routes (drift.ts
and mcp-drift.ts) validate against VALID_MCP_DRIFT_DECISIONS exported from
the resolver module, ensuring route-level and resolver-level consistency.

P2: Rename stale F240 anchor to F249 in acp-mcp-resolver test.

Added 3 contract tests verifying VALID_MCP_DRIFT_DECISIONS enum coverage.

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

* fix(zts212653#712): close validation gap in /api/drift/resolve + route-level tests

P1: /api/drift/resolve silently accepted non-array resolutions (object,
string, number) by only checking Array.isArray — non-array values
skipped validation and fell through to default use-global behavior.

Fix:
- Moved validation before drift check (fail-early on malformed input)
- Added non-array rejection, MAX_RESOLUTIONS cap, enum validation
- Both routes (drift.ts + mcp-drift.ts) now have identical validation

Route-level tests (8 cases via Fastify inject):
- Valid use-global/keep-project accepted
- Non-array resolutions → 400
- String resolutions → 400
- Invalid decision values → 400 with enum hint
- Missing mcpId → 400
- Over-limit array → 400
- Absent resolutions passes through

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant