Cross-Clan Dependencies
Paired-lifecycle entities for work that crosses clan boundaries. Synced via Jungle MCP. Vocabulary discipline: see VOCABULARY.md. Reference: METHOD.md for full methodology.
Definition
A CrossClanDependency is a single entity that two clans see from both sides, with a paired lifecycle and Jungle MCP sync keeping both copies consistent. When Persona needs a service-account API key from Cortex, when ROM Web ships a change that needs to back-port to ROM Mobile, when Howler asks Cortex for a new TTS backend — these are not Issues on one side or messages between the two. They are coordination work with a shape of their own, and they get a primitive of their own.
The innovation is the paired lifecycle. State changes on one side propagate to the other automatically. Neither clan can drift unaware of the other's view. When the target clan picks up the work (requested → in_progress_by_them), the originating clan sees the transition in their own dashboard. When the target clan ships (in_progress_by_them → delivered), the originating clan sees the delivery and decides whether it meets the need. The paired lifecycle solves the audit's most common cross-clan failure mode: one-sided drift, where the dependency's state lives only in one clan's head and the other clan changes priorities without anyone knowing.
A CrossClanDependency is not an Issue. Issues are defects on one side — a bug filed against a specific codebase. CrossClanDependencies are coordination across sides, often with no bug involved. Persona is not bug-reporting against Cortex; Persona needs a thing Cortex hasn't delivered yet. The lifecycles are different: Issues have diagnostic_state (open / investigating / resolved). CrossClanDependencies have a 7-state paired lifecycle (5 forward + 2 terminal/error).
A CrossClanDependency is not a Jungle message. Messages are ephemeral and lose state when an Alpha rolls over. CrossClanDependencies persist; the entity survives Alpha changes on either side because it lives in the Jungle MCP canonical store, not in either clan's inbox.
Sync mechanism. Jungle MCP is the source-of-truth bus. Each clan's local CrossClanDependency record syncs from the Jungle-side canonical record via a shared sync_id. Local rows for the same logical dependency in two clans both carry the same sync_id — this is the join key. The last_sync_at timestamp and last_sync_error field (populated when state transitions into sync_failed) make sync health observable. Sync failures surface as Attention Alerts in the Signal layer — both clans see the failure, and either can drive the resolution via the Retry Sync action.
Lifecycle states. The 7-state machine moves forward through normal flow, supports cancelled as an explicit terminal for abandoned dependencies, and surfaces sync_failed as a recoverable error state.
- requested — originating clan filed the dependency. Target clan hasn't picked it up.
- in_progress_by_them — target clan acknowledged and assigned an owner.
- delivered — target clan says "done from our side."
- acked — originating clan confirms delivery meets the need.
- done — terminal. Both sides done.
- cancelled — terminal. Dependency was abandoned (priorities changed, scope eliminated). Reachable from any non-terminal state.
- sync_failed — error. The Jungle MCP sync failed; the dep needs a Retry Sync action to recover. Reachable from any non-terminal state.
Backward retry transitions are allowed from sync_failed (recovery returns to a valid prior non-terminal state). done and cancelled are strictly terminal.
Field Table
| Field | Type | Required | Default | Description | Notes |
|---|---|---|---|---|---|
| id | uuid | yes | — | Local identifier (each clan has its own local id for the same logical dep) | — |
| sync_id | uuid | no | null | Cross-clan join key (Jungle-assigned). Populated on first successful publish. | — |
| originating_clan_id | text | yes | — | Clan/workspace identifier of the requester | — |
| target_clan_id | text | yes | — | Clan/workspace identifier of the provider | — |
| direction | enum | yes | — | outgoing / incoming — this workspace's perspective |
See direction enum below |
| state | enum | yes | requested | Paired lifecycle state | See state enum below |
| title | string | yes | — | Human-readable summary of the dependency | Max 200 chars |
| description | text | yes | — | What's needed, why, what acceptance looks like | — |
| acceptance_criteria | text | no | null | What "done" means from the originator's side | Optional — some deps are clear from title + description |
| state_changed_at | timestamp | yes | now() | When state last changed | — |
| state_changed_by | text | yes | — | Identifier of who/what made the change (user:<id>, system:cross-clan-sync, claude-code@<clan>) |
— |
| requested_at | timestamp | yes | now() | When dependency was filed | — |
| delivered_at | timestamp | no | null | When target_clan marked delivered |
— |
| acked_at | timestamp | no | null | When originating_clan acked delivery |
— |
| done_at | timestamp | no | null | Terminal done state |
— |
| last_sync_at | timestamp | no | null | Last successful Jungle MCP sync | Server-managed |
| last_sync_error | text | no | null | Error message when state=sync_failed | Server-managed |
| linked_outcome_ids | UUID[] | no | — | Outcomes on THIS side affected by this dependency. Stored via the cross_clan_dependency_outcome_links junction table. |
M:M link table; see Relationships section |
state enum
| Value | Meaning |
|---|---|
requested |
Filed by originating_clan. Awaiting target pickup. |
in_progress_by_them |
Target clan acknowledged and started work. |
delivered |
Target says it's done from their side. Awaiting originator's ack. |
acked |
Originating clan confirms delivery meets the need. |
done |
Terminal. Both sides done. |
cancelled |
Terminal. Dependency was abandoned. |
sync_failed |
Error. Jungle sync failed; needs Retry Sync action. |
Phase 16 reconciliation: the original draft listed 5 states (REQUESTED → IN_PROGRESS_BY_THEM → DELIVERED → ACKED → DONE) and a separate sync_status enum (synced/pending/failed/unknown). The shipped v7.0 form folds sync health into the primary state machine as a first-class sync_failed state, adds cancelled as an explicit terminal for abandoned deps, and uses snake_case throughout. The last_sync_at + last_sync_error fields replace the separate sync_status enum — when state=sync_failed, last_sync_error explains why. See VOCABULARY.md ### CrossClanDependency.state enum values.
direction enum
| Value | Meaning |
|---|---|
outgoing |
This workspace is the originator — requesting work from another clan. |
incoming |
Another clan filed a dependency on us — we are the target. |
Each clan has its own local row for the same logical dependency, joined across clans by sync_id. The direction value reflects perspective: an outgoing row in Persona's workspace corresponds to an incoming row in Cortex's workspace (same sync_id, mirrored direction).
Relationships and link tables
Outcomes link to CrossClanDependencies via the cross_clan_dependency_outcome_links junction table (Phase 16 / CCD-04). Each side maintains its own Outcome linkage — the originating clan's linked Outcomes are the ones blocked on the dependency; the target clan's linked Outcomes are the ones doing the fulfillment work. The link table is local-side only (each clan tracks its own Outcomes); the cross-clan join is via sync_id on the parent CCD row.
Link table columns: id, workspace_id (RLS), cross_clan_dependency_id, outcome_id, created_at. Unique on (cross_clan_dependency_id, outcome_id).
Phase 16 reconciliation: the original draft included linked_outcome_id_requesting and linked_outcome_id_target as single-value uuid columns on the parent row. The shipped v7.0 form uses a dedicated M:M link table matching the Invariant / Initiative / LivingProcess <-> Outcome shape from Phases 13/14/15. One dependency may block multiple Outcomes on one side, or one Outcome may depend on multiple cross-clan deps — both shapes are valid.
This table is the source of truth. Phase 16 derives the Zod schema in @canopy/shared directly from this table. The Jungle MCP sync contract on either side marshals/unmarshals records matching this Field Table exactly.
Examples
Deep example: Persona → Cortex API key dependency
Persona's prod workspace requires a Cortex API key with rate limits sufficient for the launch target. The pre-v7.0 reality: Persona logged "blocked on Cortex API key" in their open-items file. Cortex had no awareness of the blocking. Persona's Outcome stalled. The dependency lived in Persona's head and Persona's todo list, nowhere else.
The CrossClanDependency record (Persona's outgoing view; Cortex's incoming view mirrors the same sync_id):
| Field | Value |
|---|---|
| sync_id | j_cce_8b91f4... (Jungle-assigned) |
| originating_clan_id | persona |
| target_clan_id | cortex |
| direction | outgoing (Persona view) / incoming (Cortex view) |
| title | Persona needs a service-account API key for production Cortex calls |
| description | Persona's prod workspace requires a Cortex API key with rate limits sufficient for our 10k req/day target. Need key, rate-limit configuration, and renewal cadence documented. |
| acceptance_criteria | Key issued, rate limits set to 10k/day, renewal cadence documented in shared runbook. |
| state | requested → in_progress_by_them → delivered → acked → done |
| requested_at | 2026-04-01T09:00:00Z |
| delivered_at | 2026-04-02T11:00:00Z (key issued, rate limits set, doc written) |
| acked_at | 2026-04-02T16:30:00Z (Persona validated in staging) |
| done_at | 2026-04-02T16:30:00Z |
| linked_outcome_ids | [PER-XX "Persona prod workspace provisioned"] on Persona's side; [COR-XX "Production API key issued for Persona"] on Cortex's side |
| last_sync_at | 2026-04-02T16:30:02Z |
| last_sync_error | null |
Why isn't this an Issue? Persona's Outcome is blocked, but there's no bug — Cortex hasn't delivered yet. Filing an Issue on either side mis-models the situation. An Issue against Cortex says "Cortex has a defect." An Issue against Persona says "Persona has a defect." Neither is true. The CrossClanDependency captures it correctly: "Persona is requesting work from Cortex; Cortex is delivering it." The lifecycle reflects the coordination shape, not a bug shape.
Why isn't this a Jungle message? A message would be lost when Persona's Alpha or Cortex's Alpha rolled over. The persistent entity survives. The fact that the message was the v0.x mechanism — and that messages got lost — is exactly the audit observation that promoted this to a primitive.
Audit reference: 7-clan ad-hoc patterns documented in VALIDATION.md; v7.0 decision in DECISIONS-RESOLVED.md#D-01.
Short examples
ROM Web ↔ ROM Mobile back-port pairs. Changes in ROM Web that need to flow to ROM Mobile (and vice versa). Pre-v7.0, each clan maintained its own Incoming/Outgoing tables in
.clan-alpha/state/open-items.md; periodic divergence required a human to merge. Now: one CrossClanDependency per back-port pair, synced via Jungle MCP, both sides see the same state (joined by sync_id). The state machine handles "delivered but not yet validated in the target codebase" cleanly via thedelivered → ackedtransition.Howler → Cortex TTS backend. Howler's
.tickets/HOW-1-cortex-tts-backend-500.mdre-modeled as a CrossClanDependency. Pre-v7.0, Howler messaged Cortex via Jungle MCP: "need a TTS backend." Cortex's Alpha rolled over a week later; the message was lost; Howler reopened the same ask via a different channel. Now: persistent entity, both sides see it across Alpha rollovers.Multi-clan release coordination. Jungle needs a feature shipped across 3 dependent clans before a council-tier release. One CrossClanDependency per dependent. Each pair has its own lifecycle; the release proceeds when all three reach
done. The rollup view answers "is the release unblocked?" without scanning three clans separately.
Anti-patterns
Anti-pattern: One-sided tracking
Persona logged "blocked on Cortex API key" in their open-items file. Cortex had no awareness of the blocking. The dependency lived in one clan's head and one clan's todo list, nowhere else. Cortex changed priorities; Persona's Outcome stalled silently; the eventual unblock required a human escalation that should have been a routine state transition.
The correct framing: file a CrossClanDependency. Both clans see the same entity (joined by sync_id). State transitions propagate via Jungle MCP. Neither side can drift unaware of the other.
Anti-pattern: Treating cross-clan work as a Jungle message
Howler messaged Cortex via Jungle MCP: "need a TTS backend." Cortex's Alpha rolled over a week later; the message was lost. Howler reopened the same ask via a different channel. Two weeks of latency on what should have been a routine dependency.
The correct framing: persistent CrossClanDependency entity with lifecycle. Messages are for ephemeral coordination; persistent work needs a persistent entity. The entity survives Alpha rollovers on either side because it lives in the Jungle MCP canonical store, not in either clan's inbox.
Anti-pattern: Manual reconciliation between paired files
ROM Web and ROM Mobile each maintained their own Incoming/Outgoing tables in .clan-alpha/state/open-items.md. Periodic divergence required a human to merge. Six months in, the two files disagreed about ~15% of the open pairs. Reconciliation cost a half-day every two weeks. Sometimes the wrong side won the merge.
The correct framing: one synced entity per pair; Jungle MCP keeps both sides honest; sync failures surface as state=sync_failed with last_sync_error populated; no manual reconciliation step exists or is needed.
Anti-pattern: Hiding sync errors with auto-retry
Letting a sync_failed row silently retry forever hides real coordination problems (wrong target clan id, malformed sync_id, Jungle clan rolled over the canonical record, etc.). The shipped v7.0 form surfaces sync_failed as a first-class state visible on the dashboard with explicit Retry Sync action. Humans intervene.
The correct framing: sync_failed surfaces in Signal + dashboard; either clan can drive the resolution; the Retry Sync action is explicit and audited.
Cross-refs
- METHOD.md — CrossClanDependency joins the 9 method primitives
- OUTCOMES.md — Outcomes link to CrossClanDependency via cross_clan_dependency_outcome_links (M:M)
- FLOW-AND-SIGNAL.md — sync_failed CCDs and stalled deps surface as Attention Alerts
- BUGS.md — CrossClanDependency vs Issue disambiguation
- VOCABULARY.md — preferred terms + the CrossClanDependency.state enum spec
- PORTFOLIO.md — clan context for Persona, Cortex, Howler, ROM Web, ROM Mobile
docs/methodology/validation/VALIDATION.md— 7-clan audit evidencedocs/methodology/validation/DECISIONS-RESOLVED.md— D-01 origin decision
Relationship to Phase 16
Phase 16 (CrossClanDependency Primitive + Jungle Sync) ships this contract end-to-end:
- The Zod schema in
@canopy/shared/src/validators/cross-clan-dependency.ts - The Drizzle table in
@canopy/db/src/schema/cross-clan-dependency.ts - The state-machine guard in
@canopy/shared/src/utils/cross-clan-fsm.ts(single source for legal transitions) - The tRPC procedures in
@canopy/api/src/routers/cross-clan-dependency.ts(CRUD + transition + linkOutcome/unlinkOutcome + retrySync + pollSync + dashboard) - The sync adapter layer in
@canopy/api/src/services/cross-clan-sync.ts—JungleMcpAdapter(production) +NoOpAdapter(dev/fallback), factory with env-based selection and health-check fallback - The CrossClanDependency UI in
apps/web/app/(dashboard)/[workspaceSlug]/[projectSlug]/cross-clan-dependencies/— Outgoing/Incoming tabbed list, detail page with transition controls and Retry Sync, dashboard
Sync layer semantics (per CONTEXT.md):
publish()is synchronous (router awaits sync_id back).pushStateChange()is fire-and-forget (errors mark state=sync_failed).pollInbox()is admin-triggered viapollSyncmutation.CROSS_CLAN_SYNC_ENABLED=falseis the safe default → NoOpAdapter.- Health-check fallback: a failed JungleMcpAdapter.healthCheck() at boot falls back to NoOp for the process lifetime; entity remains fully functional, sync dormant.
The Jungle-side coordination (canonical store schema, webhook receiver, clan discovery) is tracked in 16-JUNGLE-INTEGRATION-HANDOFF.md.