diff --git a/examples/worktree-proximity/README.md b/examples/worktree-proximity/README.md new file mode 100644 index 000000000..149422987 --- /dev/null +++ b/examples/worktree-proximity/README.md @@ -0,0 +1,118 @@ +# Worktree proximity + +This advanced composition reference coordinates one root task and two child +agents working in linked worktrees of the same Git repository. Application +code records topology and current intent, detects path or dependency overlap, +warns the actor handling the current event, and publishes a durable notice +addressed to the other actor. The notice ledger attempts delivery on that +actor's next admitted event. No daemon and no native directed-message API are +required. + +This example is intentionally not part of the newcomer path. + +## Scenario + +1. A `session/start` event records the root actor. +2. Two `agent/start` events bind native child actor IDs to distinct worktrees. +3. `tool/before` records current path and dependency intent. +4. The pure proximity domain compares active intents from different worktrees. +5. A conflict renders an `Agent.Context` warning with an `outcome: continue` + result and publishes a recipient-scoped notice. +6. The other actor's next event admits the pending notice, changes its + evidence-backed state to `attempted`, and renders its content as context. +7. `tool/after` records an empty current intent, and `stop` marks the actor + stopped. + +The demonstration dependency convention is a `deps:` string in tool input: + +```json +{ "file_path": "src/shared.ts", "deps": "deps:react,zod" } +``` + +`Write`, `Edit`, and `Read` contribute `file_path`. Dependency names are +trimmed and compared case-insensitively. Paths are normalized to +repository-relative slash-separated paths by the domain module. + +## Architecture + +The application has four planes: + +- **Providers** — `git-worktree` derives repository, branch, commit, common + Git directory, and linked-worktree identity without throwing for expected + degradation. `agent-topology` reports that its snapshot is unavailable + before request mounting. +- **Events** — canonical shared-runtime routes observe actors, bind worktrees, + record or clear intent, detect conflicts, render current-actor context, and + publish or admit notices. +- **State and notices** — one workspace-durable topology definition and the + framework notice definition share the generated runtime's SQLite driver. + Routes use only the mounted `(await agent()).state` and + `(await agent()).notices` handles; SQLite supplies cross-process durability + and idempotency without a daemon. +- **Domain** — `src/domain/proximity.ts` contains all collision decisions and + performs no I/O. + +The generated runtime owns the durable root. It mounts SQLite at +`$AGENT_BUNDLE_PLUGIN_ROOT/state`, with the generated artifact root as the +fallback anchor, and mounts topology state and the notice ledger over that +same driver. The application never opens a second store from Git identity +data; `gitWorktree.commonDir` remains identity evidence only. + +The issue sketch places a snapshot at `providers.agentTopology.snapshot`, but +providers execute before request state is mounted, so this provider reports +an honest unavailable result and routes read snapshots from +`(await agent()).state.read()` instead. + +`worktree()` in `src/api.ts` is the issue-mandated custom Promise API over the +provider value. A `useWorktree()` React-hook variant is recorded unavailable: +the framework exposes no client-hook contract for provider values. + +## Actor identity and provenance + +Every identity claim records whether it came from a native envelope or was +derived: + +- `session/start` observes `session:` as the root actor. +- `agent/start` requires native `agent_id` and `session_id`, records the child, + and records its parent session provenance as native. +- Tool envelopes contain no `agent_id`. A tool event first resolves an active + actor already bound to the event worktree. Without an earlier binding it + uses the explicit derived identity `worktree:` and records that + provenance; it never upgrades the derived identity to native. +- `agent/start` without native identity records an `edgeRefused` event and + renders that parent identity is unavailable. It refuses to fabricate a + topology edge. + +Unsupported worktree, actor, parent, state, and delivery conditions are +rendered as unavailable instead of being replaced with invented evidence. + +## Framework primitive wiring + +Generated route workers mount the extracted `src/state.ts` definition and the +notice ledger into every request scope. `withTopology` and `withNotices` are +small capability adapters over those real handles. If a surface has no +mounted handle, they return an unavailable result and the route renders that +reason as `Agent.Context`; there is no fallback write path. + +Notice admission runs once per event invocation in the render scope. +Recipient matching uses the actor identity mounted in that request, and +`(await agent()).notices.read()` exposes only deliveries attempted for that +invocation. The coordinator status therefore reports topology facts only and +does not claim a whole-ledger pending count. + +## Evidence boundary + +The route-unit suite is in-process real-renderer evidence: it compiles the +manifest, mounts the real state and notice handles, renders the event routes, +and exercises the documented journeys against one shared durable runtime +owner. The multi-process artifact/contract suite is the next slice and has +not run here. Neither level is proof that Claude, Codex, or another commercial +host dispatches a hook, preserves its envelope, or displays projected context +in production. + +## External-driver boundary + +Version 1 connects no external driver adapter and claims none. A future +external adapter must pass the framework state-driver conformance suite +before any “integrated” claim. The generated runtime's SQLite driver is the +only durable driver used by this example. diff --git a/examples/worktree-proximity/agent-bundle.config.ts b/examples/worktree-proximity/agent-bundle.config.ts new file mode 100644 index 000000000..757e82625 --- /dev/null +++ b/examples/worktree-proximity/agent-bundle.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'agent-bundle/config'; + +export default defineConfig({ + plugin: { + description: 'Coordinate related agents across linked worktrees without a daemon.', + name: 'worktree-proximity', + version: '1.0.0', + }, + runtime: { node: '22.19.0' }, + targets: ['claude', 'codex'], +}); diff --git a/examples/worktree-proximity/package.json b/examples/worktree-proximity/package.json new file mode 100644 index 000000000..20067a134 --- /dev/null +++ b/examples/worktree-proximity/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agent-bundle-example/worktree-proximity", + "version": "1.0.0", + "private": true, + "description": "An advanced worktree-proximity coordination reference for agent-bundle.", + "type": "module", + "engines": { + "node": ">=22.19.0" + }, + "files": [ + "artifact", + "README.md" + ], + "scripts": { + "build": "agent-bundle build --output artifact", + "check": "pnpm validate && pnpm build && pnpm typecheck && pnpm test && pnpm test:routes", + "dev": "agent-bundle dev", + "test": "rstest tests --exclude 'tests/route-unit/**'", + "test:routes": "rstest --config rstest.route-unit.config.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "validate": "agent-bundle validate" + }, + "dependencies": { + "@agent-bundle/runtime": "workspace:*", + "react": "19.2.8", + "zod": "4.4.3" + }, + "devDependencies": { + "@rstest/core": "0.11.10", + "@types/react": "19.2.18", + "agent-bundle": "workspace:*" + } +} diff --git a/examples/worktree-proximity/rstest.route-unit.config.ts b/examples/worktree-proximity/rstest.route-unit.config.ts new file mode 100644 index 000000000..d4adc77ee --- /dev/null +++ b/examples/worktree-proximity/rstest.route-unit.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@rstest/core'; +import { agentBundleRstest } from 'agent-bundle/rstest'; + +export default defineConfig(await agentBundleRstest()); diff --git a/examples/worktree-proximity/src/api.ts b/examples/worktree-proximity/src/api.ts new file mode 100644 index 000000000..3da19beb9 --- /dev/null +++ b/examples/worktree-proximity/src/api.ts @@ -0,0 +1,36 @@ +import { agent } from '@agent-bundle/runtime'; +import { z } from 'zod'; + +export const WorktreeProviderValueSchema = z.discriminatedUnion('state', [ + z + .object({ + branch: z.string().min(1), + commonDir: z.string().min(1), + head: z.string().min(1), + isLinkedWorktree: z.boolean(), + root: z.string().min(1), + source: z.enum(['native-cwd', 'process-cwd']), + state: z.literal('available'), + }) + .strict(), + z + .object({ + reason: z.string().min(1), + state: z.literal('unavailable'), + }) + .strict(), +]); + +export type WorktreeProviderValue = z.output; +export type AvailableWorktree = Extract; + +export const worktree = async (): Promise => { + const candidate = (await agent()).providers.gitWorktree; + const parsed = WorktreeProviderValueSchema.safeParse(candidate); + return parsed.success + ? parsed.data + : { + reason: 'The git-worktree provider did not expose a valid worktree identity.', + state: 'unavailable', + }; +}; diff --git a/examples/worktree-proximity/src/coordination.ts b/examples/worktree-proximity/src/coordination.ts new file mode 100644 index 000000000..4ae8d4d57 --- /dev/null +++ b/examples/worktree-proximity/src/coordination.ts @@ -0,0 +1,73 @@ +import { + agent, + type AgentStateHandle, +} from '@agent-bundle/runtime'; +import type { AgentNoticesHandle } from '@agent-bundle/runtime/notices'; + +import { + type TopologyEvents, + type TopologyState, +} from './state.js'; + +export type TopologyAccess = + Pick, 'dispatch' | 'read'>; + +export type CapabilityResult = + | { + readonly state: 'available'; + readonly value: T; + } + | { + readonly reason: string; + readonly state: 'unavailable'; + }; + +export const withTopology = async ( + operation: (topology: TopologyAccess) => Promise, +): Promise> => { + const context = await agent(); + if (context.state === undefined) { + return { + reason: 'Topology state unavailable: this request has no mounted state handle.', + state: 'unavailable', + }; + } + try { + return { + state: 'available', + value: await operation( + context.state as AgentStateHandle, + ), + }; + } catch (error) { + return { + reason: + `Topology state unavailable: ${error instanceof Error ? error.message : String(error)}`, + state: 'unavailable', + }; + } +}; + +export const withNotices = async ( + operation: (notices: AgentNoticesHandle) => Promise, +): Promise> => { + const context = await agent(); + if (context.notices === undefined) { + return { + reason: 'Directed notices unavailable: this request has no mounted notice handle.', + state: 'unavailable', + }; + } + try { + return { + state: 'available', + value: await operation(context.notices), + }; + } catch (error) { + return { + reason: + `Directed notices unavailable: ${error instanceof Error ? error.message : String(error)}`, + state: 'unavailable', + }; + } +}; diff --git a/examples/worktree-proximity/src/domain/proximity.ts b/examples/worktree-proximity/src/domain/proximity.ts new file mode 100644 index 000000000..e010ab90d --- /dev/null +++ b/examples/worktree-proximity/src/domain/proximity.ts @@ -0,0 +1,88 @@ +import type { TopologyState } from '../state.js'; + +export interface ProximityIntent { + readonly actorId: string; + readonly dependencies: readonly string[]; + readonly paths: readonly string[]; +} + +export interface ProximityConflict { + readonly actorId: string; + readonly summary: string; +} + +const normalizeSegments = (value: string): string => { + const segments: string[] = []; + for (const segment of value.replaceAll('\\', '/').split('/')) { + if (segment === '' || segment === '.') continue; + if (segment === '..' && segments.length > 0 && segments.at(-1) !== '..') { + segments.pop(); + } else { + segments.push(segment); + } + } + return segments.join('/'); +}; + +const normalizePath = (value: string, worktreeRoot: string): string => { + const path = value.replaceAll('\\', '/'); + const root = worktreeRoot.replaceAll('\\', '/').replace(/\/+$/u, ''); + const relative = path === root + ? '.' + : path.startsWith(`${root}/`) + ? path.slice(root.length + 1) + : path; + return normalizeSegments(relative); +}; + +const normalizeDependency = (value: string): string => value.trim().toLowerCase(); + +export const findProximity = ( + snapshot: TopologyState, + currentWorktree: string, + intent: ProximityIntent, +): readonly ProximityConflict[] => { + const currentPaths = new Set(intent.paths.map((path) => normalizePath(path, currentWorktree)).filter(Boolean)); + const currentDependencies = new Set( + intent.dependencies.map(normalizeDependency).filter((dependency) => dependency !== ''), + ); + const actors = new Map(snapshot.actors.map((actor) => [actor.id, actor])); + const conflicts: ProximityConflict[] = []; + + for (const activity of snapshot.activities) { + if (activity.actorId === intent.actorId) continue; + const actor = actors.get(activity.actorId); + if ( + actor === undefined + || actor.status !== 'active' + || actor.worktreeRoot === undefined + || actor.worktreeRoot === currentWorktree + ) { + continue; + } + + const sharedPath = activity.paths + .map((path) => normalizePath(path, actor.worktreeRoot!)) + .find((path) => currentPaths.has(path)); + if (sharedPath !== undefined) { + conflicts.push({ + actorId: actor.id, + summary: + `Worktrees ${currentWorktree} and ${actor.worktreeRoot} both intend to change path ${sharedPath}.`, + }); + } + + const sharedDependency = activity.dependencies + .map(normalizeDependency) + .find((dependency) => currentDependencies.has(dependency)); + if (sharedDependency !== undefined) { + conflicts.push({ + actorId: actor.id, + summary: + `Worktrees ${currentWorktree} and ${actor.worktreeRoot} both depend on ${sharedDependency}.`, + }); + } + } + + return conflicts; +}; diff --git a/examples/worktree-proximity/src/event-support.ts b/examples/worktree-proximity/src/event-support.ts new file mode 100644 index 000000000..3055781d1 --- /dev/null +++ b/examples/worktree-proximity/src/event-support.ts @@ -0,0 +1,139 @@ +import type { + AgentDocumentNode, + AgentNoticeDelivery, +} from '@agent-bundle/runtime'; + +import type { AvailableWorktree } from './api.js'; +import type { TopologyAccess } from './coordination.js'; +import type { TopologyState } from './state.js'; + +export interface EventIdentity { + readonly idempotencyKey: string; + readonly observedAt: string; +} + +export interface ExtractedIntent { + readonly dependencies: readonly string[]; + readonly paths: readonly string[]; +} + +export interface ResolvedActor { + readonly id: string; + readonly source: 'derived' | 'native'; +} + +export const nativeString = ( + native: Readonly>, + key: string, +): string | undefined => { + const value = native[key]; + return typeof value === 'string' && value.trim() !== '' ? value : undefined; +}; + +const inputRecord = (native: Readonly>): Readonly> => { + const input = native.tool_input; + return input !== null && typeof input === 'object' && !Array.isArray(input) + ? input as Readonly> + : {}; +}; + +const dependenciesFrom = (input: Readonly>): readonly string[] => { + const convention = input.deps; + if (typeof convention !== 'string' || !convention.trim().toLowerCase().startsWith('deps:')) return []; + return convention + .trim() + .slice('deps:'.length) + .split(/[\s,]+/u) + .map((dependency) => dependency.trim()) + .filter((dependency) => dependency !== ''); +}; + +export const extractIntent = ( + native: Readonly>, +): ExtractedIntent => { + const input = inputRecord(native); + const toolName = nativeString(native, 'tool_name'); + const path = input.file_path; + const paths = + (toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') + && typeof path === 'string' + && path.trim() !== '' + ? [path] + : []; + return { + dependencies: dependenciesFrom(input), + paths, + }; +}; + +export const actorForWorktree = async ( + topology: TopologyAccess, + worktree: AvailableWorktree, + canonical: EventIdentity, +): Promise<{ readonly actor: ResolvedActor; readonly snapshot: TopologyState }> => { + const before = await topology.read(); + const bound = before.state.actors.find( + (actor) => actor.status === 'active' && actor.worktreeRoot === worktree.root && actor.kind === 'child', + ) ?? before.state.actors.find( + (actor) => actor.status === 'active' && actor.worktreeRoot === worktree.root, + ); + if (bound !== undefined) { + return { + actor: { id: bound.id, source: bound.provenance.id }, + snapshot: before.state, + }; + } + + const actor: ResolvedActor = { + id: `worktree:${worktree.root}`, + source: 'derived', + }; + await topology.dispatch('actorObserved', { + id: actor.id, + kind: 'child', + provenance: { id: 'derived' }, + status: 'active', + }, { + idempotencyKey: `${canonical.idempotencyKey}:derived-actor`, + }); + const boundResult = await topology.dispatch('actorBound', { + actorId: actor.id, + provenance: 'derived', + worktreeRoot: worktree.root, + }, { + idempotencyKey: `${canonical.idempotencyKey}:derived-worktree`, + }); + return { actor, snapshot: boundResult.state }; +}; + +const nodeText = (node: AgentDocumentNode): string => { + switch (node.kind) { + case 'result': + return node.children.map(nodeText).filter(Boolean).join('\n'); + case 'context': + case 'markdown': + case 'text': + return node.text; + case 'error': + return `${node.code}: ${node.message}`; + case 'resource': + return `${node.name} (${node.uri})`; + case 'progress': + return node.message ?? ''; + case 'audio': + case 'image': + case 'json': + return ''; + default: { + const unreachable: never = node; + throw new Error(`Unhandled Agent Document node ${String(unreachable)}`); + } + } +}; + +export const deliveryContexts = ( + deliveries: readonly AgentNoticeDelivery[], +): readonly string[] => deliveries.map((delivery) => { + const text = nodeText(delivery.notice.content.root); + return `Directed proximity notice (${delivery.notice.state}, ${delivery.receipt.channel}): ${text}`; +}); diff --git a/examples/worktree-proximity/src/events/agent/start.tsx b/examples/worktree-proximity/src/events/agent/start.tsx new file mode 100644 index 000000000..0df71385e --- /dev/null +++ b/examples/worktree-proximity/src/events/agent/start.tsx @@ -0,0 +1,97 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; +import React from 'react'; + +import { worktree } from '../../api.js'; +import { withNotices, withTopology } from '../../coordination.js'; +import { deliveryContexts, nativeString } from '../../event-support.js'; + +export const config = { + runtime: 'shared', + targets: ['claude', 'codex'], +}; + +export default async function AgentStart({ + canonical, + native, +}: AgentEventRouteProps) { + const currentWorktree = await worktree(); + if (currentWorktree.state === 'unavailable') { + return ( + + {`Child topology unavailable: ${currentWorktree.reason}`} + + ); + } + + const agentId = nativeString(native, 'agent_id'); + const sessionId = nativeString(native, 'session_id'); + if (agentId === undefined || sessionId === undefined) { + const refusal = agentId === undefined + ? 'agent/start omitted native agent_id; refused to fabricate a topology edge' + : 'agent/start omitted native session_id; refused to fabricate a topology edge'; + const topologyResult = await withTopology(async (topology) => { + await topology.dispatch('edgeRefused', { + idempotencyKey: canonical.idempotencyKey, + observedAt: canonical.observedAt, + reason: refusal, + ...(sessionId === undefined ? {} : { sessionId }), + }, { + idempotencyKey: `${canonical.idempotencyKey}:refusal`, + }); + }); + const noticeResult = await withNotices(async (notices) => notices.read()); + const contexts = [ + ...(topologyResult.state === 'unavailable' ? [topologyResult.reason] : []), + ...(noticeResult.state === 'available' + ? deliveryContexts(noticeResult.value) + : [noticeResult.reason]), + ]; + return ( + + {`Parent identity unavailable; ${refusal}.`} + {contexts.map((context) => + {context})} + + ); + } + + const topologyResult = await withTopology(async (topology) => { + await topology.dispatch('actorObserved', { + id: agentId, + kind: 'child', + parentSessionId: sessionId, + provenance: { + id: 'native', + parentSessionId: 'native', + }, + status: 'active', + }, { + idempotencyKey: `${canonical.idempotencyKey}:actor`, + }); + await topology.dispatch('actorBound', { + actorId: agentId, + provenance: 'native', + worktreeRoot: currentWorktree.root, + }, { + idempotencyKey: `${canonical.idempotencyKey}:worktree`, + }); + }); + if (topologyResult.state === 'unavailable') { + return ( + + {topologyResult.reason} + + ); + } + const noticeResult = await withNotices(async (notices) => notices.read()); + const contexts = noticeResult.state === 'available' + ? deliveryContexts(noticeResult.value) + : [noticeResult.reason]; + return ( + + {contexts.map((context) => + {context})} + + ); +} diff --git a/examples/worktree-proximity/src/events/session/start.tsx b/examples/worktree-proximity/src/events/session/start.tsx new file mode 100644 index 000000000..80392a9d1 --- /dev/null +++ b/examples/worktree-proximity/src/events/session/start.tsx @@ -0,0 +1,69 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; +import React from 'react'; + +import { worktree } from '../../api.js'; +import { withNotices, withTopology } from '../../coordination.js'; +import { deliveryContexts, nativeString } from '../../event-support.js'; + +export const config = { + runtime: 'shared', + targets: ['claude', 'codex'], +}; + +export default async function SessionStart({ + canonical, + native, +}: AgentEventRouteProps) { + const currentWorktree = await worktree(); + if (currentWorktree.state === 'unavailable') { + return ( + + {`Worktree topology unavailable: ${currentWorktree.reason}`} + + ); + } + const sessionId = nativeString(native, 'session_id'); + if (sessionId === undefined) { + return ( + + Root actor unavailable: session/start omitted native session_id. + + ); + } + + const actorId = `session:${sessionId}`; + const topologyResult = await withTopology(async (topology) => { + await topology.dispatch('actorObserved', { + id: actorId, + kind: 'root', + provenance: { id: 'native' }, + status: 'active', + }, { + idempotencyKey: `${canonical.idempotencyKey}:actor`, + }); + await topology.dispatch('actorBound', { + actorId, + provenance: 'native', + worktreeRoot: currentWorktree.root, + }, { + idempotencyKey: `${canonical.idempotencyKey}:worktree`, + }); + }); + if (topologyResult.state === 'unavailable') { + return ( + + {topologyResult.reason} + + ); + } + const noticeResult = await withNotices(async (notices) => notices.read()); + const contexts = noticeResult.state === 'available' + ? deliveryContexts(noticeResult.value) + : [noticeResult.reason]; + return ( + + {contexts.map((context) => {context})} + + ); +} diff --git a/examples/worktree-proximity/src/events/stop.tsx b/examples/worktree-proximity/src/events/stop.tsx new file mode 100644 index 000000000..20ef7e52d --- /dev/null +++ b/examples/worktree-proximity/src/events/stop.tsx @@ -0,0 +1,61 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; +import React from 'react'; + +import { worktree } from '../api.js'; +import { withNotices, withTopology } from '../coordination.js'; +import { + actorForWorktree, + deliveryContexts, + nativeString, + type ResolvedActor, +} from '../event-support.js'; + +export const config = { + runtime: 'shared', + targets: ['claude', 'codex'], +}; + +export default async function Stop({ + canonical, + native, +}: AgentEventRouteProps) { + const currentWorktree = await worktree(); + if (currentWorktree.state === 'unavailable') { + return ( + + {`Actor stop unavailable: ${currentWorktree.reason}`} + + ); + } + const nativeActorId = nativeString(native, 'agent_id'); + const topologyResult = await withTopology(async (topology): Promise => { + const resolved = nativeActorId === undefined + ? (await actorForWorktree(topology, currentWorktree, canonical)).actor + : { id: nativeActorId, source: 'native' as const }; + await topology.dispatch('actorStopped', { + actorId: resolved.id, + observedAt: canonical.observedAt, + }, { + idempotencyKey: `${canonical.idempotencyKey}:stopped`, + }); + return resolved; + }); + if (topologyResult.state === 'unavailable') { + return ( + + {topologyResult.reason} + + ); + } + const noticeResult = await withNotices(async (notices) => notices.read()); + const contexts = noticeResult.state === 'available' + ? deliveryContexts(noticeResult.value) + : [noticeResult.reason]; + return ( + + {contexts.map((context) => + {context})} + + ); +} diff --git a/examples/worktree-proximity/src/events/tool/after.tsx b/examples/worktree-proximity/src/events/tool/after.tsx new file mode 100644 index 000000000..72c8bb3c4 --- /dev/null +++ b/examples/worktree-proximity/src/events/tool/after.tsx @@ -0,0 +1,60 @@ +import { Agent } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; +import React from 'react'; + +import { worktree } from '../../api.js'; +import { withNotices, withTopology } from '../../coordination.js'; +import { actorForWorktree, deliveryContexts } from '../../event-support.js'; + +export const config = { + runtime: 'shared', + targets: ['claude', 'codex'], +}; + +export default async function AfterTool({ + canonical, +}: AgentEventRouteProps) { + const currentWorktree = await worktree(); + if (currentWorktree.state === 'unavailable') { + return ( + + {`Activity completion unavailable: ${currentWorktree.reason}`} + + ); + } + const topologyResult = await withTopology(async (topology) => { + const resolved = await actorForWorktree(topology, currentWorktree, canonical); + await topology.dispatch('intentRecorded', { + actorId: resolved.actor.id, + dependencies: [], + idempotencyKey: canonical.idempotencyKey, + observedAt: canonical.observedAt, + paths: [], + provenance: { + actorId: resolved.actor.source, + dependencies: 'native', + paths: 'native', + }, + }, { + idempotencyKey: `${canonical.idempotencyKey}:completion`, + }); + return resolved.actor; + }); + if (topologyResult.state === 'unavailable') { + return ( + + {topologyResult.reason} + + ); + } + const noticeResult = await withNotices(async (notices) => notices.read()); + const contexts = noticeResult.state === 'available' + ? deliveryContexts(noticeResult.value) + : [noticeResult.reason]; + return ( + + {contexts.map((context) => + {context})} + + ); +} diff --git a/examples/worktree-proximity/src/events/tool/before.tsx b/examples/worktree-proximity/src/events/tool/before.tsx new file mode 100644 index 000000000..e7cf8632e --- /dev/null +++ b/examples/worktree-proximity/src/events/tool/before.tsx @@ -0,0 +1,106 @@ +import { Agent, type JsonValue } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; +import React from 'react'; + +import { worktree } from '../../api.js'; +import { withNotices, withTopology } from '../../coordination.js'; +import { findProximity } from '../../domain/proximity.js'; +import { + actorForWorktree, + deliveryContexts, + extractIntent, +} from '../../event-support.js'; + +export const config = { + runtime: 'shared', + targets: ['claude', 'codex'], +}; + +export default async function BeforeTool({ + canonical, + native, +}: AgentEventRouteProps) { + const currentWorktree = await worktree(); + if (currentWorktree.state === 'unavailable') { + return ( + + {`Proximity detection unavailable: ${currentWorktree.reason}`} + + ); + } + const intent = extractIntent(native); + const topologyResult = await withTopology(async (topology) => { + const { actor } = await actorForWorktree(topology, currentWorktree, canonical); + const committed = await topology.dispatch('intentRecorded', { + actorId: actor.id, + dependencies: [...intent.dependencies], + idempotencyKey: canonical.idempotencyKey, + observedAt: canonical.observedAt, + paths: [...intent.paths], + provenance: { + actorId: actor.source, + dependencies: 'native', + paths: 'native', + }, + }, { + idempotencyKey: `${canonical.idempotencyKey}:intent`, + }); + return { + actor, + conflicts: findProximity(committed.state, currentWorktree.root, { + actorId: actor.id, + dependencies: intent.dependencies, + paths: intent.paths, + }), + }; + }); + if (topologyResult.state === 'unavailable') { + return ( + + {topologyResult.reason} + + ); + } + const resolution = topologyResult.value; + + const noticeResult = await withNotices(async (notices) => { + const deliveries = await notices.read(); + for (const [index, conflict] of resolution.conflicts.entries()) { + await notices.publish({ + content: { + root: { + kind: 'text', + text: conflict.summary, + }, + status: 'success', + version: 1, + }, + dedupeKey: `proximity:${resolution.actor.id}:${conflict.actorId}:${conflict.summary}`, + priority: 'high', + recipient: { + actor: { id: conflict.actorId }, + }, + }, { + idempotencyKey: `${canonical.idempotencyKey}:notice:${String(index)}`, + }); + } + return deliveryContexts(deliveries); + }); + const deliveryAndPublication = noticeResult.state === 'available' + ? noticeResult.value + : [noticeResult.reason]; + const warnings = resolution.conflicts.map((conflict) => + `Proximity warning for ${resolution.actor.id}: ${conflict.summary}`); + const reason = warnings.join(' '); + const value: JsonValue = reason === '' + ? { outcome: 'continue' as const } + : { outcome: 'continue' as const, reason }; + + return ( + + {deliveryAndPublication.map((context) => + {context})} + {warnings.map((warning) => {warning})} + + ); +} diff --git a/examples/worktree-proximity/src/mcp/coordinator/tools/status.tsx b/examples/worktree-proximity/src/mcp/coordinator/tools/status.tsx new file mode 100644 index 000000000..16ee525da --- /dev/null +++ b/examples/worktree-proximity/src/mcp/coordinator/tools/status.tsx @@ -0,0 +1,78 @@ +import { Agent, type JsonValue } from '@agent-bundle/runtime'; +import type { ToolConfig, ToolRouteProps } from 'agent-bundle'; +import React from 'react'; +import { z } from 'zod'; + +import { withTopology } from '../../../coordination.js'; +import { ActorSchema } from '../../../state.js'; + +export const config = { + annotations: { readOnlyHint: true }, + description: 'Show the mounted durable worktree topology, active intents, and refusals.', +} satisfies ToolConfig; + +export const inputSchema = z + .object({ + actorId: z.string().min(1).optional(), + }) + .strict(); + +export const resultSchema = z + .object({ + activeActivities: z.number().int().nonnegative(), + actors: z.array(ActorSchema), + reason: z.string().optional(), + refusals: z.number().int().nonnegative(), + state: z.enum(['available', 'unavailable']), + }) + .strict(); + +type StatusResult = z.output; + +export default async function Status({ + input, +}: ToolRouteProps) { + const topologyResult = await withTopology(async (store) => (await store.read()).state); + let result: StatusResult; + if (topologyResult.state === 'unavailable') { + result = { + activeActivities: 0, + actors: [], + reason: topologyResult.reason, + refusals: 0, + state: 'unavailable', + }; + } else { + const topology = topologyResult.value; + const actors = input.actorId === undefined + ? topology.actors + : topology.actors.filter((actor) => actor.id === input.actorId); + const visibleIds = new Set(actors.map((actor) => actor.id)); + result = { + activeActivities: topology.activities.filter( + (activity) => + visibleIds.has(activity.actorId) + && (activity.paths.length > 0 || activity.dependencies.length > 0), + ).length, + actors, + refusals: topology.refusals.length, + state: 'available', + }; + } + + const markdown = result.state === 'available' + ? [ + '# Worktree proximity status', + '', + `- Actors: ${String(result.actors.length)}`, + `- Active activities: ${String(result.activeActivities)}`, + `- Refused edges: ${String(result.refusals)}`, + ].join('\n') + : `# Worktree proximity status\n\nUnavailable: ${result.reason ?? 'unknown reason'}`; + return ( + + {markdown} + + + ); +} diff --git a/examples/worktree-proximity/src/providers/agent-topology.ts b/examples/worktree-proximity/src/providers/agent-topology.ts new file mode 100644 index 000000000..31c47432f --- /dev/null +++ b/examples/worktree-proximity/src/providers/agent-topology.ts @@ -0,0 +1,12 @@ +export interface AgentTopologyProviderValue { + readonly reason: string; + readonly state: 'unavailable'; +} + +export default function agentTopologyProvider(): AgentTopologyProviderValue { + return { + reason: + 'Topology snapshots are available only from the mounted request state handle; providers execute before that handle is mounted.', + state: 'unavailable', + }; +} diff --git a/examples/worktree-proximity/src/providers/git-worktree.ts b/examples/worktree-proximity/src/providers/git-worktree.ts new file mode 100644 index 000000000..ff1afd90b --- /dev/null +++ b/examples/worktree-proximity/src/providers/git-worktree.ts @@ -0,0 +1,78 @@ +import { execFile } from 'node:child_process'; +import { resolve } from 'node:path'; +import { promisify } from 'node:util'; + +import type { WorktreeProviderValue } from '../api.js'; + +interface ProviderContext { + readonly invocation: { + readonly kind: string; + readonly props: Readonly>; + }; + readonly signal: AbortSignal; +} + +const execFileAsync = promisify(execFile); + +const eventCwd = (context: ProviderContext): string | undefined => { + if (context.invocation.kind !== 'event') return undefined; + const payload = context.invocation.props.payload; + if (payload === null || typeof payload !== 'object') return undefined; + const native = (payload as { readonly native?: unknown }).native; + if (native === null || typeof native !== 'object') return undefined; + const cwd = (native as { readonly cwd?: unknown }).cwd; + return typeof cwd === 'string' && cwd.trim() !== '' ? cwd : undefined; +}; + +const absoluteGitPath = (cwd: string, value: string): string => + resolve(cwd, value); + +export default async function gitWorktreeProvider( + context: ProviderContext, +): Promise { + const nativeCwd = eventCwd(context); + const cwd = nativeCwd ?? process.cwd(); + const source = nativeCwd === undefined ? 'process-cwd' : 'native-cwd'; + if (cwd.trim() === '') { + return { + reason: 'No working directory was available for git worktree discovery.', + state: 'unavailable', + }; + } + + const git = async (...args: readonly string[]): Promise => { + const result = await execFileAsync( + 'git', + ['-C', cwd, ...args], + { encoding: 'utf8', signal: context.signal }, + ); + return result.stdout.trim(); + }; + + try { + const [root, branch, head, commonDirValue, gitDirValue] = await Promise.all([ + git('rev-parse', '--show-toplevel'), + git('rev-parse', '--abbrev-ref', 'HEAD'), + git('rev-parse', 'HEAD'), + git('rev-parse', '--git-common-dir'), + git('rev-parse', '--git-dir'), + ]); + const commonDir = absoluteGitPath(cwd, commonDirValue); + const gitDir = absoluteGitPath(cwd, gitDirValue); + return { + branch, + commonDir, + head, + isLinkedWorktree: gitDir !== commonDir, + root: resolve(root), + source, + state: 'available', + }; + } catch (error) { + return { + reason: + `Git worktree identity is unavailable for ${cwd}: ${error instanceof Error ? error.message : String(error)}`, + state: 'unavailable', + }; + } +} diff --git a/examples/worktree-proximity/src/state.ts b/examples/worktree-proximity/src/state.ts new file mode 100644 index 000000000..0f51e26af --- /dev/null +++ b/examples/worktree-proximity/src/state.ts @@ -0,0 +1,172 @@ +import { defineState } from '@agent-bundle/runtime/state'; +import { z } from 'zod'; + +const nonEmpty = z.string().trim().min(1); +const provenance = z.enum(['native', 'derived']); + +export const ActorSchema = z + .object({ + id: nonEmpty, + kind: z.enum(['root', 'child']), + parentSessionId: nonEmpty.optional(), + provenance: z + .object({ + id: provenance, + parentSessionId: provenance.optional(), + worktreeRoot: provenance.optional(), + }) + .strict(), + status: z.enum(['active', 'stopped']), + worktreeRoot: nonEmpty.optional(), + }) + .strict(); + +export const ActivitySchema = z + .object({ + actorId: nonEmpty, + dependencies: z.array(nonEmpty), + idempotencyKey: nonEmpty, + observedAt: nonEmpty, + paths: z.array(nonEmpty), + provenance: z + .object({ + actorId: provenance, + dependencies: provenance, + paths: provenance, + }) + .strict(), + }) + .strict(); + +export const EdgeRefusalSchema = z + .object({ + idempotencyKey: nonEmpty, + observedAt: nonEmpty, + reason: nonEmpty, + sessionId: nonEmpty.optional(), + }) + .strict(); + +export const TopologyStateSchema = z + .object({ + activities: z.array(ActivitySchema), + actors: z.array(ActorSchema), + refusals: z.array(EdgeRefusalSchema), + }) + .strict(); + +export type Actor = z.output; +export type Activity = z.output; +export type EdgeRefusal = z.output; +export type TopologyState = z.output; + +const actorObservedSchema = ActorSchema.omit({ worktreeRoot: true }).strict(); +const actorBoundSchema = z + .object({ + actorId: nonEmpty, + provenance, + worktreeRoot: nonEmpty, + }) + .strict(); +const actorStoppedSchema = z + .object({ + actorId: nonEmpty, + observedAt: nonEmpty, + }) + .strict(); + +export const topologyEventSchemas = { + actorBound: actorBoundSchema, + actorObserved: actorObservedSchema, + actorStopped: actorStoppedSchema, + edgeRefused: EdgeRefusalSchema, + intentRecorded: ActivitySchema, +} as const; + +export type TopologyEvents = typeof topologyEventSchemas; + +const replaceActor = ( + actors: readonly Actor[], + actorId: string, + update: (actor: Actor) => Actor, +): Actor[] => actors.map((actor) => actor.id === actorId ? update(actor) : actor); + +export const topologyStateDefinition = defineState({ + events: topologyEventSchemas, + id: 'worktree-proximity/topology', + initial: { + activities: [], + actors: [], + refusals: [], + }, + lifetime: 'workspace-durable', + reduce: (state, event): TopologyState => { + switch (event.name) { + case 'actorObserved': { + const previous = state.actors.find((actor) => actor.id === event.payload.id); + const actor = previous === undefined + ? event.payload + : { + ...event.payload, + ...(previous.worktreeRoot === undefined ? {} : { worktreeRoot: previous.worktreeRoot }), + provenance: { + ...event.payload.provenance, + ...(previous.provenance.worktreeRoot === undefined + ? {} + : { worktreeRoot: previous.provenance.worktreeRoot }), + }, + }; + return { + ...state, + actors: [...state.actors.filter((candidate) => candidate.id !== actor.id), actor], + }; + } + case 'actorBound': + return { + ...state, + actors: replaceActor(state.actors, event.payload.actorId, (actor) => ({ + ...actor, + provenance: { + ...actor.provenance, + worktreeRoot: event.payload.provenance, + }, + worktreeRoot: event.payload.worktreeRoot, + })), + }; + case 'intentRecorded': + return { + ...state, + activities: [ + ...state.activities.filter((activity) => activity.actorId !== event.payload.actorId), + event.payload, + ], + }; + case 'actorStopped': + return { + ...state, + activities: state.activities.filter((activity) => activity.actorId !== event.payload.actorId), + actors: replaceActor(state.actors, event.payload.actorId, (actor) => ({ + ...actor, + status: 'stopped', + })), + }; + case 'edgeRefused': + return { + ...state, + refusals: [...state.refusals, event.payload], + }; + default: { + const unreachable: never = event; + throw new Error(`Unhandled topology event ${String(unreachable)}`); + } + } + }, + schema: TopologyStateSchema, + version: 1, +}); + +export default defineState({ + ...topologyStateDefinition, + id: 'worktree-proximity/topology', + lifetime: 'workspace-durable', +}); diff --git a/examples/worktree-proximity/tests/proximity.test.ts b/examples/worktree-proximity/tests/proximity.test.ts new file mode 100644 index 000000000..6063dd3c2 --- /dev/null +++ b/examples/worktree-proximity/tests/proximity.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from '@rstest/core'; + +import { findProximity } from '../src/domain/proximity.js'; +import type { TopologyState } from '../src/state.js'; + +const snapshot = (path: string, dependency: string): TopologyState => ({ + activities: [{ + actorId: 'agent-b', + dependencies: [dependency], + idempotencyKey: 'intent-b', + observedAt: '2026-09-01T20:00:00.000Z', + paths: [path], + provenance: { + actorId: 'native', + dependencies: 'native', + paths: 'native', + }, + }], + actors: [ + { + id: 'agent-a', + kind: 'child', + parentSessionId: 'root-session', + provenance: { + id: 'native', + parentSessionId: 'native', + worktreeRoot: 'native', + }, + status: 'active', + worktreeRoot: '/repo/worktrees/a', + }, + { + id: 'agent-b', + kind: 'child', + parentSessionId: 'root-session', + provenance: { + id: 'native', + parentSessionId: 'native', + worktreeRoot: 'native', + }, + status: 'active', + worktreeRoot: '/repo/worktrees/b', + }, + ], + refusals: [], +}); + +describe('findProximity', () => { + it('returns no conflict for distinct paths and dependencies', () => { + expect(findProximity( + snapshot('src/catalog.ts', 'zod'), + '/repo/worktrees/a', + { actorId: 'agent-a', dependencies: ['react'], paths: ['src/player.ts'] }, + )).toEqual([]); + }); + + it('reports a normalized repo-relative path overlap in another worktree', () => { + expect(findProximity( + snapshot('./src/shared.ts', 'zod'), + '/repo/worktrees/a', + { actorId: 'agent-a', dependencies: [], paths: ['/repo/worktrees/a/src/shared.ts'] }, + )).toEqual([{ + actorId: 'agent-b', + summary: + 'Worktrees /repo/worktrees/a and /repo/worktrees/b both intend to change path src/shared.ts.', + }]); + }); + + it('reports a case-insensitive dependency overlap in another worktree', () => { + expect(findProximity( + snapshot('src/catalog.ts', 'Zod'), + '/repo/worktrees/a', + { actorId: 'agent-a', dependencies: ['zod'], paths: [] }, + )).toEqual([{ + actorId: 'agent-b', + summary: + 'Worktrees /repo/worktrees/a and /repo/worktrees/b both depend on zod.', + }]); + }); + + it('ignores an actor in the same worktree', () => { + const sameWorktree: TopologyState = { + ...snapshot('src/shared.ts', 'zod'), + actors: snapshot('src/shared.ts', 'zod').actors.map((actor) => ({ + ...actor, + worktreeRoot: '/repo/worktrees/a', + })), + }; + expect(findProximity( + sameWorktree, + '/repo/worktrees/a', + { actorId: 'agent-a', dependencies: ['zod'], paths: ['src/shared.ts'] }, + )).toEqual([]); + }); +}); diff --git a/examples/worktree-proximity/tests/route-unit/routes.test.ts b/examples/worktree-proximity/tests/route-unit/routes.test.ts new file mode 100644 index 000000000..30da9c34d --- /dev/null +++ b/examples/worktree-proximity/tests/route-unit/routes.test.ts @@ -0,0 +1,405 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; +import { available } from '@agent-bundle/runtime'; +import { + createGeneratedRuntimeState, + type GeneratedRuntimeState, +} from '@agent-bundle/runtime/mount'; +import { createSqliteStateDriver } from '@agent-bundle/runtime/state/sqlite'; +import { expectDocument, renderRoute, testManifest } from 'agent-bundle/test'; + +import BeforeTool from '../../src/events/tool/before.js'; +import { + topologyStateDefinition, + type TopologyEvents, + type TopologyState, +} from '../../src/state.js'; + +const manifest = testManifest(); + +const worktrees = { + root: '/repo', + a: '/repo/.worktrees/a', + b: '/repo/.worktrees/b', +} as const; + +let stateRoot: string; +let runtimeState: GeneratedRuntimeState; +let sequence = 0; + +const provider = (root: string) => ({ + branch: `branch-${root.split('/').at(-1) ?? 'root'}`, + commonDir: '/repo/.git', + head: '490cb102ebb247d4d0b1a4ce5178ddfb66c9e5dd', + isLinkedWorktree: root !== worktrees.root, + root, + source: 'native-cwd' as const, + state: 'available' as const, +}); + +const eventInput = ( + event: 'agent/start' | 'session/start' | 'stop' | 'tool/after' | 'tool/before', + native: Record, + id: string, +) => ({ + canonical: { + event, + idempotencyKey: id, + observedAt: `2026-09-01T20:00:${String(sequence++).padStart(2, '0')}.000Z`, + provenance: { + host: 'claude', + hostContractRevision: 'route-unit', + nativeEvent: native.hook_event_name as string, + source: 'native', + }, + sequence, + }, + native, +}); + +const renderEventInput = async ( + route: string, + input: ReturnType, + id: string, + worktreeRoot: string, + actorId?: string, +) => { + const bindings = await runtimeState.requestBindings(); + try { + return await renderRoute(route, { + context: { + actor: actorId === undefined ? undefined : available({ id: actorId }, 'native'), + host: available({ name: 'claude' }, 'native'), + invocation: { + id: `invocation:${id}`, + startedAt: `2026-09-01T20:01:${String(sequence).padStart(2, '0')}.000Z`, + }, + noticeLedger: bindings.noticeLedger, + providers: { gitWorktree: provider(worktreeRoot) }, + session: available({ sessionId: 'root-session' }, 'native'), + state: bindings.state, + workspace: available({ root: '/repo' }, 'native'), + }, + input, + }); + } finally { + await bindings.close(); + } +}; + +const renderEvent = ( + route: string, + event: Parameters[0], + native: Record, + id: string, + worktreeRoot: string, + actorId?: string, +) => renderEventInput( + route, + eventInput(event, native, id), + id, + worktreeRoot, + actorId, +); + +const bindActors = async (): Promise => { + await renderEvent( + 'event:session/start', + 'session/start', + { cwd: worktrees.root, hook_event_name: 'SessionStart', session_id: 'root-session' }, + 'root:start', + worktrees.root, + 'session:root-session', + ); + await renderEvent( + 'event:agent/start', + 'agent/start', + { + agent_id: 'agent-a', + agent_type: 'implementation', + cwd: worktrees.a, + hook_event_name: 'SubagentStart', + session_id: 'root-session', + }, + 'agent-a:start', + worktrees.a, + 'agent-a', + ); + await renderEvent( + 'event:agent/start', + 'agent/start', + { + agent_id: 'agent-b', + agent_type: 'implementation', + cwd: worktrees.b, + hook_event_name: 'SubagentStart', + session_id: 'root-session', + }, + 'agent-b:start', + worktrees.b, + 'agent-b', + ); +}; + +const recordIntent = ( + actorId: 'agent-a' | 'agent-b', + root: string, + path: string, + id: string, + deps = '', +) => renderEvent( + 'event:tool/before', + 'tool/before', + { + cwd: root, + hook_event_name: 'PreToolUse', + session_id: 'root-session', + tool_input: { deps, file_path: path }, + tool_name: 'Edit', + }, + id, + root, + actorId, +); + +beforeEach(async () => { + stateRoot = await mkdtemp(join(tmpdir(), 'worktree-proximity-route-unit-')); + runtimeState = createGeneratedRuntimeState({ + definition: topologyStateDefinition, + driver: createSqliteStateDriver({ root: stateRoot }), + }); + sequence = 0; +}); + +afterEach(async () => { + await runtimeState.close(); + await rm(stateRoot, { force: true, recursive: true }); +}); + +it('compiles the complete shared-runtime route surface', () => { + expect(manifest.diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual([]); + expect(Object.keys(manifest.routes)).toEqual(expect.arrayContaining([ + 'event:agent/start', + 'event:session/start', + 'event:stop', + 'event:tool/after', + 'event:tool/before', + 'tool:coordinator/status', + ])); +}); + +describe('worktree proximity journeys', () => { + it('does not warn when active paths and dependencies do not overlap (journey 3)', async () => { + await bindActors(); + const first = await recordIntent('agent-a', worktrees.a, 'src/player.ts', 'intent:a', 'deps:react'); + const rendered = await recordIntent('agent-b', worktrees.b, 'src/catalog.ts', 'intent:b', 'deps:zod'); + + expectDocument(first).toHaveStatus('success').toHaveNodeKinds(['result']); + expect(first.document.value).toEqual({ outcome: 'continue' }); + expectDocument(rendered).toHaveStatus('success').toHaveNodeKinds(['result']); + expect(rendered.document.value).toEqual({ outcome: 'continue' }); + }); + + it('warns without denying and publishes a pending directed notice (journeys 4 and 5)', async () => { + await bindActors(); + const first = await recordIntent('agent-a', worktrees.a, 'src/shared.ts', 'intent:a'); + const rendered = await recordIntent('agent-b', worktrees.b, 'src/shared.ts', 'intent:b'); + + expectDocument(first).toHaveStatus('success').toHaveNodeKinds(['result']); + expect(first.document.value).toEqual({ outcome: 'continue' }); + expectDocument(rendered) + .toHaveStatus('success') + .toContainContext('Proximity warning') + .toContainContext('src/shared.ts'); + expect(rendered.document.value).toMatchObject({ + outcome: 'continue', + reason: expect.stringContaining('src/shared.ts'), + }); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.noticeLedger.read(); + expect(snapshot.notices).toEqual([ + expect.objectContaining({ + recipient: { actor: { id: 'agent-a' } }, + state: 'pending', + }), + ]); + } finally { + await bindings.close(); + } + }); + + it('attempts and surfaces a notice on the recipient next event (journey 6)', async () => { + await bindActors(); + await recordIntent('agent-a', worktrees.a, 'src/shared.ts', 'intent:a'); + await recordIntent('agent-b', worktrees.b, 'src/shared.ts', 'intent:b'); + + const delivered = await renderEvent( + 'event:tool/after', + 'tool/after', + { + cwd: worktrees.a, + hook_event_name: 'PostToolUse', + session_id: 'root-session', + tool_input: { file_path: 'src/shared.ts' }, + tool_name: 'Edit', + }, + 'intent:a:after', + worktrees.a, + 'agent-a', + ); + + expectDocument(delivered) + .toHaveStatus('success') + .toContainContext('Directed proximity notice') + .toContainContext('src/shared.ts'); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.noticeLedger.read(); + expect(snapshot.notices[0]).toMatchObject({ + attempts: [expect.objectContaining({ invocationId: 'invocation:intent:a:after' })], + state: 'attempted', + }); + } finally { + await bindings.close(); + } + }); + + it('deduplicates a repeated native intent envelope (journey 7)', async () => { + await bindActors(); + const replayed = eventInput( + 'tool/before', + { + cwd: worktrees.a, + hook_event_name: 'PreToolUse', + session_id: 'root-session', + tool_input: { file_path: 'src/player.ts' }, + tool_name: 'Edit', + }, + 'intent:replayed', + ); + await renderEventInput( + 'event:tool/before', + replayed, + 'intent:replayed', + worktrees.a, + 'agent-a', + ); + await renderEventInput( + 'event:tool/before', + replayed, + 'intent:replayed', + worktrees.a, + 'agent-a', + ); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.state.read(); + expect(snapshot.state.activities.filter((activity) => activity.actorId === 'agent-a')).toHaveLength(1); + } finally { + await bindings.close(); + } + }); + + it('records a refusal and never fabricates an edge without native agent identity (journey 8)', async () => { + const rendered = await renderEvent( + 'event:agent/start', + 'agent/start', + { + agent_type: 'fixture-without-identity', + cwd: worktrees.a, + hook_event_name: 'SubagentStart', + session_id: 'root-session', + }, + 'agent:missing-id', + worktrees.a, + ); + + expectDocument(rendered) + .toHaveStatus('success') + .toContainContext('Parent identity unavailable') + .toContainContext('refused to fabricate'); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.state.read(); + expect(snapshot.state.actors).toEqual([]); + expect(snapshot.state.refusals).toEqual([ + expect.objectContaining({ + reason: 'agent/start omitted native agent_id; refused to fabricate a topology edge', + }), + ]); + } finally { + await bindings.close(); + } + }); + + it('renders the coordinator status from mounted topology state', async () => { + await bindActors(); + await recordIntent('agent-a', worktrees.a, 'src/shared.ts', 'intent:a'); + await recordIntent('agent-b', worktrees.b, 'src/shared.ts', 'intent:b'); + + const bindings = await runtimeState.requestBindings(); + let rendered: Awaited>; + try { + rendered = await renderRoute('tool:coordinator/status', { + context: { + noticeLedger: bindings.noticeLedger, + providers: { gitWorktree: provider(worktrees.root) }, + state: bindings.state, + }, + input: {}, + }); + } finally { + await bindings.close(); + } + + expectDocument(rendered) + .toHaveStatus('success') + .toContainMarkdown('Worktree proximity status'); + expect(rendered.result).toMatchObject({ + activeActivities: 2, + actors: expect.arrayContaining([ + expect.objectContaining({ id: 'agent-a', worktreeRoot: worktrees.a }), + expect.objectContaining({ id: 'agent-b', worktreeRoot: worktrees.b }), + ]), + refusals: 0, + }); + }); + + it('renders state unavailability when an event module has no mounted handle', async () => { + const rendered = await renderRoute({ default: BeforeTool }, { + context: { + actor: available({ id: 'agent-a' }, 'native'), + providers: { gitWorktree: provider(worktrees.a) }, + }, + input: eventInput( + 'tool/before', + { + cwd: worktrees.a, + hook_event_name: 'PreToolUse', + session_id: 'root-session', + tool_input: { file_path: 'src/shared.ts' }, + tool_name: 'Edit', + }, + 'intent:unmounted', + ), + kind: 'event-route', + routeId: 'event:tool/before', + }); + + expectDocument(rendered) + .toHaveStatus('success') + .toContainContext('state unavailable'); + expect(rendered.document.value).toMatchObject({ + outcome: 'continue', + reason: expect.stringContaining('state unavailable'), + }); + }); +}); diff --git a/examples/worktree-proximity/tsconfig.json b/examples/worktree-proximity/tsconfig.json new file mode 100644 index 000000000..67cbb7fcf --- /dev/null +++ b/examples/worktree-proximity/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, + "include": [ + "agent-bundle.config.ts", + "src/**/*.ts", + "src/**/*.tsx", + "tests/**/*.ts", + "tests/**/*.tsx" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d805d3d28..1fb8d377f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,6 +197,28 @@ importers: specifier: workspace:* version: link:../../packages/agent-bundle + examples/worktree-proximity: + dependencies: + '@agent-bundle/runtime': + specifier: workspace:* + version: link:../../packages/rsc-runtime + react: + specifier: 19.2.8 + version: 19.2.8 + zod: + specifier: 4.4.3 + version: 4.4.3 + devDependencies: + '@rstest/core': + specifier: 0.11.10 + version: 0.11.10 + '@types/react': + specifier: 19.2.18 + version: 19.2.18 + agent-bundle: + specifier: workspace:* + version: link:../../packages/agent-bundle + packages/agent-bundle: dependencies: '@agent-bundle/runtime': @@ -761,16 +783,6 @@ packages: core-js: optional: true - '@rsbuild/core@2.2.0': - resolution: {integrity: sha512-UnBBfxWIDKVdLz2BUBq7hFBatwLclJ4moFhlDFg+pFBPPJ1g34MmCbGUC0c9Mo1DhPGdYJG69qMIldh5MvC74w==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - core-js: '>= 3.0.0' - peerDependenciesMeta: - core-js: - optional: true - '@rsbuild/core@2.2.1': resolution: {integrity: sha512-JcGtG4bo7PBihj6fBL6gaxaJihqLf7nGWW/t4zEmXpMJkV6XNdr1jAo9B0xI4mLAOmtFeva8cUbn9SE2ZEmAIw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -860,11 +872,6 @@ packages: cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-arm64@2.2.0': - resolution: {integrity: sha512-KAVVT7hp3NBjtc/RY2UtOjzzc8i+s4pIhW1p52UV+Aev6ywQCu3dXwkHTonpPvJO3hqLXc4zIMH5l4HbMqBm4g==} - cpu: [arm64] - os: [darwin] - '@rspack/binding-darwin-arm64@2.2.1': resolution: {integrity: sha512-Y/Naw/7V76QiUYdYRuBzBZtzRjt/3fjDUuF8GK0+/BO7BP1RrpY4tk1ln+iiqegRUD8u9uGn08fi8No1rwfyUg==} cpu: [arm64] @@ -875,11 +882,6 @@ packages: cpu: [x64] os: [darwin] - '@rspack/binding-darwin-x64@2.2.0': - resolution: {integrity: sha512-rzyJCX99aFwl540trsVMNZOgK4+IFm2d5+YeP+RdNo9Uprxloz8vHz0J4dYtaq6MRiCAyM60dAwEa3wJMwqWAQ==} - cpu: [x64] - os: [darwin] - '@rspack/binding-darwin-x64@2.2.1': resolution: {integrity: sha512-rTIG/xZIW7RbEEuMR9hnNn5dv3fDBpX0N4FAQUwfhUYy3tN2+3vibTTq/Nj1Sd9Vn4yWydbWIEwBX6m/aGejig==} cpu: [x64] @@ -891,12 +893,6 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-arm64-gnu@2.2.0': - resolution: {integrity: sha512-0t8QOiOMcBV7RvPSsTJ5DQ4QCK6FIyUZy77qbxnS6asGTOXPZZn7V5cL26IxEv/wuHdQ6tQOXheau1fi+gGyBQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rspack/binding-linux-arm64-gnu@2.2.1': resolution: {integrity: sha512-53rAMU6Hqiat21IMU1hTt4Si2F33h+ZbXOt+Y18W39AFjcwmleIBId2u7beqJeGXLJuBgIAm31jcbJj/PN7mWQ==} cpu: [arm64] @@ -909,12 +905,6 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-arm64-musl@2.2.0': - resolution: {integrity: sha512-BAvCukqcuHxUdE294ITCohvhVkEklW8RbkKkR36Uo0WyIiMPGrnvPjARPn0/4Q4xMAz7lUmC60sZrvJHlAOKMw==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rspack/binding-linux-arm64-musl@2.2.1': resolution: {integrity: sha512-o7zFiWkt4MqSfSdTbxUdF27fcrWKpRizcuVB8H3yd2G6xW9V2OfYbhVGQnOlbKi+GK74RkCmoJtANB+QboIqKQ==} cpu: [arm64] @@ -927,12 +917,6 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-ppc64-gnu@2.2.0': - resolution: {integrity: sha512-nCHqZLv/E8nm2ccGkb00F5DQtXxzGy3W3X73ArA+N0+zXJUnzRcSRSwr7AE8pVgP/FYfX4yMFgUXy0g0YxYGRA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rspack/binding-linux-ppc64-gnu@2.2.1': resolution: {integrity: sha512-pvx1oeg1z7cr8OcNt7PAt1SJTHzdsN/pvu7HkGnfks2fWE7GDs9DLL2KvN7tUkzQvJm6bGgUwqSCOrqV4uSwAg==} cpu: [ppc64] @@ -945,12 +929,6 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-riscv64-gnu@2.2.0': - resolution: {integrity: sha512-CA3WEqKFDI6FAZTnCho2n9pmdPWZYAW/S8mqgxd0cx2Jix43at3VyLxhCC7ED5A9WBSFn/AdHaIbVtgoQHVhWA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@rspack/binding-linux-riscv64-gnu@2.2.1': resolution: {integrity: sha512-WQ6P94Wz2tgwOsgifTWP2/bV63iZH5+rkxngQwF22FC8KmIXXy/Yug7lyMH1ld8Hzg4p1qXjBBr4i1cCyJTR+w==} cpu: [riscv64] @@ -963,12 +941,6 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-riscv64-musl@2.2.0': - resolution: {integrity: sha512-kHB960oClkoPRPZ6sdkhRvqbdRIlbpIMYd/Tbxfmn3DWQahiCk1pkUFJbOtFq3EgESxZISV4THl442W2Y57HvQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - '@rspack/binding-linux-riscv64-musl@2.2.1': resolution: {integrity: sha512-GEFUFHQkjKU7OYyHXnrIo8wWcUHM7jeKov5z6Lxd3i+3hKo11yBOgb7b+uD94Rx2tPuWF4jyFdWmcNu46Njb/A==} cpu: [riscv64] @@ -981,12 +953,6 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-s390x-gnu@2.2.0': - resolution: {integrity: sha512-lVBdiffVo1jq0P0jT36jNou2suLB4ueQI4aWUs+HM+h67YPBtVKWu/mo5Wh59+8nowgcZmYaFM5hdH69963I9w==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rspack/binding-linux-s390x-gnu@2.2.1': resolution: {integrity: sha512-Cqw2UjSmFGZaw1EjQXClKDud86ThynGEEIazy2PZfPzZG4WjICK1WjdeFCJd7xWsSbUQ3jGyzb7/EHiDVa+sKA==} cpu: [s390x] @@ -999,12 +965,6 @@ packages: os: [linux] libc: [glibc] - '@rspack/binding-linux-x64-gnu@2.2.0': - resolution: {integrity: sha512-M49UaWspE0YJ3268DsquD8idEQTfjBDMvO/I8qccV/Z5T+Q98FJ+kIs5liUaTWb48OIbDEK+8ZKx5QzLbfVN6g==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rspack/binding-linux-x64-gnu@2.2.1': resolution: {integrity: sha512-QX+gRxg2CS9ri2fUG5eNurdsrOCWoJ56SsD2O7kcVGiRm+S2+muNb/QhkdkAdt/u/m++7Ve5TMIpHTnaKYCpCw==} cpu: [x64] @@ -1017,12 +977,6 @@ packages: os: [linux] libc: [musl] - '@rspack/binding-linux-x64-musl@2.2.0': - resolution: {integrity: sha512-YYbs0wmey+5blhEQDE4Dax3TwJtqfGwe2QBm3OLphlBHo/fcZVvimzKkMV0/pVrZTLy2z5ZAwNhGMY64bNr77w==} - cpu: [x64] - os: [linux] - libc: [musl] - '@rspack/binding-linux-x64-musl@2.2.1': resolution: {integrity: sha512-mBZQl1NdGbEB3y5M9d0tkuF7RL1GLz3Hb3gqFa3QRZBymP9PCV83Jiji8s2PJimNUJIVcdZRIw8+VGYs/35c2A==} cpu: [x64] @@ -1033,10 +987,6 @@ packages: resolution: {integrity: sha512-KY5YbWbuvYcoaLXnV+vzZOvGRCeb6jt4EpVpKdph1h1IJjwX/ju15EQ+GOe3iecZEdf0OttQcNVcwBkLkFT9ag==} cpu: [wasm32] - '@rspack/binding-wasm32-wasi@2.2.0': - resolution: {integrity: sha512-rerLPTN/HD4EvLNWs3O2N+Eb37eGvLRIP3dXXc3n+UzTebOepAsahNn44vXeRBsE4m/pHkpDJjwgWTytgQ2gBw==} - cpu: [wasm32] - '@rspack/binding-wasm32-wasi@2.2.1': resolution: {integrity: sha512-/d2ImKDS+lT+FJ07MxKBeUkTat84tr2Nm2+nIRt8HmZK7/N8odkQml9vb4MHr8E7oYOp4B+jm6W5frdRzKrkJQ==} cpu: [wasm32] @@ -1046,11 +996,6 @@ packages: cpu: [arm64] os: [win32] - '@rspack/binding-win32-arm64-msvc@2.2.0': - resolution: {integrity: sha512-JUAmnbOQYGTRyX28vls/MOMonZWcmcCi5YtEq6YMc8Xqh3Qx0HUwaLM/I1xr/N9BX3b8CV0dQDOpNuBc2ei+CA==} - cpu: [arm64] - os: [win32] - '@rspack/binding-win32-arm64-msvc@2.2.1': resolution: {integrity: sha512-TfmaKPF3KC7uoZb6A+8ZUbLS8g8P5EdeXFGLCaJ+UgdkJ20TsapcfJXZXWNnKzFEkV8dUO/t5oNxNLYy2URusw==} cpu: [arm64] @@ -1061,11 +1006,6 @@ packages: cpu: [ia32] os: [win32] - '@rspack/binding-win32-ia32-msvc@2.2.0': - resolution: {integrity: sha512-wOmQRUaOG0eWH/fnfslA9yK9xKfaq9X+3Xa1TdTJnTqlo0ARJYs6A+Lzjbs7cxdY/o1f12Xe00BG3nQozReUOg==} - cpu: [ia32] - os: [win32] - '@rspack/binding-win32-ia32-msvc@2.2.1': resolution: {integrity: sha512-rdBXayngvpQFMSUIC4b71FDZrSBJszSHNEkln/Nis3a17DMAE7IkgJcqe7SqLWbk2OPF/N920AOWmBEDQQCaMg==} cpu: [ia32] @@ -1076,11 +1016,6 @@ packages: cpu: [x64] os: [win32] - '@rspack/binding-win32-x64-msvc@2.2.0': - resolution: {integrity: sha512-v6/3bFr9+i7hRpgulL9b5qCvZL0VgR4vQGQNqOWezUzZmPUj9LYpvB0L9xZIVwDQ2ug/xBiA58bfg5IbESgoyw==} - cpu: [x64] - os: [win32] - '@rspack/binding-win32-x64-msvc@2.2.1': resolution: {integrity: sha512-l3K4s7nrQJc+3LacPFjZGjX8Jk1sf8Q4TK6IXAsdSucOjTqYSpD8PMl2NUXCBDXOl8T2V4v323z1LfxwW8BPmA==} cpu: [x64] @@ -1089,9 +1024,6 @@ packages: '@rspack/binding@2.1.10': resolution: {integrity: sha512-vnu/UP5HnrND15lO9+VeG6eUrbTyycHNQNQ3XEiRiFojuoiGZkIZC3Hbzr8qQH44C6vScPODEPvvIVvLcO2LpQ==} - '@rspack/binding@2.2.0': - resolution: {integrity: sha512-nxZzJqqB0EmEKp6qjzFNkBb/SgGt0k0DSENrLvAJgvVvrm3waVsubD0cfxtPlZY/rd5SzadzxWGEHRyFcds5nA==} - '@rspack/binding@2.2.1': resolution: {integrity: sha512-56TqztuEMd+aHGv1jDXnkJQGSLTb4NoO146flFxJqPG8931UdXPO2pNR9M0Q2Pz+GvmO0fLHGPLYBHoRVrRlHw==} @@ -1107,18 +1039,6 @@ packages: '@swc/helpers': optional: true - '@rspack/core@2.2.0': - resolution: {integrity: sha512-3W7oX0BAHbK4VlknH3lfyfRvupzxdZtyEa+DfKmdjzmIAcqYtHnFd0nLqp5dzitDPyDI1TIKkDhpB0AZJn0pVg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 - '@swc/helpers': ^0.5.23 - peerDependenciesMeta: - '@module-federation/runtime-tools': - optional: true - '@swc/helpers': - optional: true - '@rspack/core@2.2.1': resolution: {integrity: sha512-EHFX2oWCY1HkHJkG/Ev8HXCcl4gQzgETyairdIlBkKaypuW8i7kowBxUFzpHHg/ObF70vB3focToel9Wwg4MRA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3100,13 +3020,6 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.2.0': - dependencies: - '@rspack/core': 2.2.0(@swc/helpers@0.5.23) - '@swc/helpers': 0.5.23 - transitivePeerDependencies: - - '@module-federation/runtime-tools' - '@rsbuild/core@2.2.1': dependencies: '@rspack/core': 2.2.1(@swc/helpers@0.5.23) @@ -3175,90 +3088,60 @@ snapshots: '@rspack/binding-darwin-arm64@2.1.10': optional: true - '@rspack/binding-darwin-arm64@2.2.0': - optional: true - '@rspack/binding-darwin-arm64@2.2.1': optional: true '@rspack/binding-darwin-x64@2.1.10': optional: true - '@rspack/binding-darwin-x64@2.2.0': - optional: true - '@rspack/binding-darwin-x64@2.2.1': optional: true '@rspack/binding-linux-arm64-gnu@2.1.10': optional: true - '@rspack/binding-linux-arm64-gnu@2.2.0': - optional: true - '@rspack/binding-linux-arm64-gnu@2.2.1': optional: true '@rspack/binding-linux-arm64-musl@2.1.10': optional: true - '@rspack/binding-linux-arm64-musl@2.2.0': - optional: true - '@rspack/binding-linux-arm64-musl@2.2.1': optional: true '@rspack/binding-linux-ppc64-gnu@2.1.10': optional: true - '@rspack/binding-linux-ppc64-gnu@2.2.0': - optional: true - '@rspack/binding-linux-ppc64-gnu@2.2.1': optional: true '@rspack/binding-linux-riscv64-gnu@2.1.10': optional: true - '@rspack/binding-linux-riscv64-gnu@2.2.0': - optional: true - '@rspack/binding-linux-riscv64-gnu@2.2.1': optional: true '@rspack/binding-linux-riscv64-musl@2.1.10': optional: true - '@rspack/binding-linux-riscv64-musl@2.2.0': - optional: true - '@rspack/binding-linux-riscv64-musl@2.2.1': optional: true '@rspack/binding-linux-s390x-gnu@2.1.10': optional: true - '@rspack/binding-linux-s390x-gnu@2.2.0': - optional: true - '@rspack/binding-linux-s390x-gnu@2.2.1': optional: true '@rspack/binding-linux-x64-gnu@2.1.10': optional: true - '@rspack/binding-linux-x64-gnu@2.2.0': - optional: true - '@rspack/binding-linux-x64-gnu@2.2.1': optional: true '@rspack/binding-linux-x64-musl@2.1.10': optional: true - '@rspack/binding-linux-x64-musl@2.2.0': - optional: true - '@rspack/binding-linux-x64-musl@2.2.1': optional: true @@ -3269,13 +3152,6 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true - '@rspack/binding-wasm32-wasi@2.2.0': - dependencies: - '@emnapi/core': 1.11.3 - '@emnapi/runtime': 1.11.3 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) - optional: true - '@rspack/binding-wasm32-wasi@2.2.1': dependencies: '@emnapi/core': 1.11.3 @@ -3286,27 +3162,18 @@ snapshots: '@rspack/binding-win32-arm64-msvc@2.1.10': optional: true - '@rspack/binding-win32-arm64-msvc@2.2.0': - optional: true - '@rspack/binding-win32-arm64-msvc@2.2.1': optional: true '@rspack/binding-win32-ia32-msvc@2.1.10': optional: true - '@rspack/binding-win32-ia32-msvc@2.2.0': - optional: true - '@rspack/binding-win32-ia32-msvc@2.2.1': optional: true '@rspack/binding-win32-x64-msvc@2.1.10': optional: true - '@rspack/binding-win32-x64-msvc@2.2.0': - optional: true - '@rspack/binding-win32-x64-msvc@2.2.1': optional: true @@ -3327,23 +3194,6 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.1.10 '@rspack/binding-win32-x64-msvc': 2.1.10 - '@rspack/binding@2.2.0': - optionalDependencies: - '@rspack/binding-darwin-arm64': 2.2.0 - '@rspack/binding-darwin-x64': 2.2.0 - '@rspack/binding-linux-arm64-gnu': 2.2.0 - '@rspack/binding-linux-arm64-musl': 2.2.0 - '@rspack/binding-linux-ppc64-gnu': 2.2.0 - '@rspack/binding-linux-riscv64-gnu': 2.2.0 - '@rspack/binding-linux-riscv64-musl': 2.2.0 - '@rspack/binding-linux-s390x-gnu': 2.2.0 - '@rspack/binding-linux-x64-gnu': 2.2.0 - '@rspack/binding-linux-x64-musl': 2.2.0 - '@rspack/binding-wasm32-wasi': 2.2.0 - '@rspack/binding-win32-arm64-msvc': 2.2.0 - '@rspack/binding-win32-ia32-msvc': 2.2.0 - '@rspack/binding-win32-x64-msvc': 2.2.0 - '@rspack/binding@2.2.1': optionalDependencies: '@rspack/binding-darwin-arm64': 2.2.1 @@ -3367,12 +3217,6 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.23 - '@rspack/core@2.2.0(@swc/helpers@0.5.23)': - dependencies: - '@rspack/binding': 2.2.0 - optionalDependencies: - '@swc/helpers': 0.5.23 - '@rspack/core@2.2.1(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.2.1 @@ -3419,7 +3263,7 @@ snapshots: '@rstest/core@0.11.10': dependencies: - '@rsbuild/core': 2.2.0 + '@rsbuild/core': 2.2.1 '@types/chai': 5.2.3 transitivePeerDependencies: - '@module-federation/runtime-tools'