Elnor Repo Reader

DOC3_App_Skills_R11_2_Canonical.md

Current Specs/DOC3/DOC3_App_Skills_R11_2_Canonical.md

Short text page dfefc9b8b35c. Generated 2026-06-09T01:23:58.539Z from commit dbaa25962edc11ab30e8d4ca1715f9ae5bf77331. Worktree: clean.

Open readable HTML page · Open raw txt · Open path URL

ELNOR REPO READER TEXT MIRROR
Original path: Current Specs/DOC3/DOC3_App_Skills_R11_2_Canonical.md
Source repo: /Users/OpenClaw1/Elnor/Elnor Specs
Git branch: main
Git commit: dbaa25962edc11ab30e8d4ca1715f9ae5bf77331
Generated: 2026-06-09T01:23:58.539Z

---

# DOC3 App Skills — R11.2 [Canonical Consolidated]

## Revision Lineage (must persist in all later versions)

Based on:
- DOC3 App Skills — R11.1 [Canonical Consolidated]
- DOC3_FINAL_SELF_CONTAINED_REDLINE_R3_2
- integrated multi-round red-team review, adjudication, self-audit, and production-flattening decisions covering:
  - canonical LearnSession lifecycle and later-use routing
  - proposal / validation / install / catalog / availability completion
  - MCP / M365 / Acrobat contract completion
  - staged upload / import / trigger-test / skill packs completion
  - control-backing, read-surface backing, and non-phantom UI rules
  - compile-safety, migration-hardening, and cross-doc owner closure

This consolidated version fully supersedes DOC3 App Skills — R11.1 as the operative DOC3 specification.

## Consolidation Rule

This file is the current **single operative DOC3** specification.

If any preserved historical content below conflicts with **Part 0 — R11.2 Canonical Reconciliation, Contract Completion, and Production Hardening**, Part 0 governs.

## Included Source Chain

- 1. Inherited Baseline — DOC3 App Skills — R11.1 [Canonical Consolidated]
- 2. Integrated Production Revision — R11.2 Canonical Reconciliation, Contract Completion, and Production Hardening

## Canonicalization Note

R11.2 is the production-flattened successor to the prior DOC3 base plus the full operative redline stack. It removes patch-layer indirection by carrying forward the settled contracts directly in the operative body. No Appendix-only DOC3 contract remains authoritative after this merge.

---

# Part 0 — R11.2 Canonical Reconciliation, Contract Completion, and Production Hardening

## §0B Purpose

R11 materially improved the architecture, but review of the canonical DOC3 / companion pair and subsequent design clarification exposed remaining friction in four areas:

1. overlapping user-facing learning labels,
2. drift risk between `TeachSession`-style legacy concepts and the newer `LearnSession`,
3. incomplete single-object runtime truth for end-to-end learning visibility,
4. overcomplicated "sandbox" framing for app rehearsal, especially for complex local apps.

R11.2 resolves those problems while preserving:
- OpenClaw-native-first,
- EC single-writer,
- capability/skill packaging,
- MCP as a first-class connector surface,
- autonomous skill mining,
- and the broader DOC3/DOC10/DOC11/DOC7/DOC8/DOC9/DOC15 architecture.

## §0B.1 Non-negotiable invariants

These remain law:

1. **OpenClaw-native-first**
2. **EC is the canonical durable writer**
3. **Q is not a second runtime authority**
4. **Skills are reusable abilities, not runtime primitives**
5. **Project / matter identity remains outside DOC3**
6. **Observation outside the agent is explicit, bounded, and visible**
7. **Generated abilities are proposals first; no silent shared installs**
8. **Every learned ability must become runtime-truthful before it can be claimed as usable**

## §0B.2 User-facing information architecture (authoritative)

The user-facing information architecture is already canonical and remains:

# Actions & Abilities

Within **Actions & Abilities**, the browser/second column must expose:

## Actions
- Active
- Tasks
- Automations

## Abilities
- Skills
- Connectors
- Learn

### Authoritative clarification
R11 already carried this structure. R11.2 does **not** change the left-nav or browser grouping.  
R11.2 carries forward the Learn naming cleanup and completes the missing contracts needed for production use.

## §0B.3 Learn is the user entry; Ability Learning is the page title

Authoritative user-facing rule:

- Left-nav / browser label: **Learn**
- The main-pane page title opened by Learn: **Ability Learning**

Avoid using **Learning Center** as the browser or left-nav label.  
"Ability Learning" may be used as the page header or subtitle if desired.

## §0B.4 No hard-coded product-agent name in generic UI

Default user-facing learning UI must be agent-name agnostic.

### Forbidden as default hard-coded labels
- Teach Elnor
- Show Elnor
- Elnor Practice
- Improve Elnor Skill

### Allowed as tenant/branding aliases only
A deployment may inject a branded agent display name, but the canonical default labels remain generic.

Recommended UI token:
```ts
export interface AgentBrandingConfig {
  agentDisplayName?: string; // optional runtime branding only
  schemaVersion: 1;
}
```

If `agentDisplayName` is absent, generic labels must be used.

## §0B.5 Canonical user-facing learning modes

The system must expose exactly five top-level user-facing creation/import actions under **Abilities → Learn → Learn New Ability**.

### User-facing heading
> **Create new ability by:**

### Canonical cards
1. **Demonstrating Skill**
2. **Coaching Agent**
3. **Autonomous Agent Practice**
4. **Improving Existing Skill**
5. **Import Skill**

### Clarification
These are **user-facing entry modes**, not separate backend runtime object types.

## §0B.6 Contextual shortcuts vs top-level learning modes

The following labels may exist as contextual shortcuts, but are **not** top-level learning modes:

- **Learn From This**
- **Use as Example**
- **Improve This Skill**
- **Watch My Actions**

### Authoritative semantics
- **Learn From This** launches a new `LearnSession` from a completed run, trace set, receipt, or recent action.
- **Watch My Actions** is a capture/observation option within relevant learning modes. It is **not** a standalone primary learning mode.
- **Observe My Actions** in inherited body is a deprecated label; treat it as `observation_mode = outside_agent` attached to the relevant learning mode.

## §0B.7 Pending Abilities replaces experimental/private user-facing labels

User-facing UI should not use confusing primary labels such as:
- Experimental
- Experimental Abilities
- Private Trial Skill
- Experimental Skill

### Authoritative user-facing grouping
Anything not yet finally accepted and promoted into **Skills** should appear under:

> **Pending Abilities**

This includes:
- active drafts,
- proposals awaiting input,
- proposals under testing,
- revisions of existing skills,
- privately active learned skills that are still not promoted,
- imported skills awaiting review/acceptance.

### Important distinction
Internal install lanes may still retain richer backend values like `experimental_private`, but those are implementation states, not primary user-facing labels.

## §0B.8 Canonical Learn page tabs

Inside **Abilities → Learn**, the canonical top-level tabs are:

1. **Learn New Ability**
2. **Active Capability Learning**
3. **Pending Abilities**
4. **History**

### Definitions
- **Learn New Ability** = launch new learning/import flows
- **Active Capability Learning** = in-flight sessions, captures, testing, or clarification
- **Pending Abilities** = all draft/review/private-active/revision items not yet promoted to Skills
- **History** = prior sessions, receipts, accepted/rejected outcomes, archived learning events

### Explicit rule
Do **not** expose **Installed** or **Experimental** as primary Learn-page tabs.
Approved/promoted installed abilities belong under **Abilities → Skills**.

## §0B.9 Skills page semantics

**Abilities → Skills** must show:
- native OpenClaw skills,
- imported skills that are accepted/installed,
- taught/learned skills that are accepted/promoted,
- bundled skills,
- disabled skills,
- quarantined skills if user filters allow.

It may also expose source filters like:
- Native
- Imported
- Learned
- Bundle
- Disabled
- Quarantined

But it must not be the primary home for in-progress learning artifacts.

## §0B.10 LearnSession is the only canonical learning runtime object

R11 preserved some inherited references that make it appear as if there are parallel canonical runtime objects for "teach" and "learn". R11.2 keeps the R11.1 canonicalization and closes the remaining lifecycle / route / later-use gaps.

### Authoritative rule
There is exactly **one** canonical runtime object for active learning orchestration:

> `LearnSession`

### Deprecation rule
- `TeachSession` is not a separate canonical runtime object.
- "Teach" remains a UX concept / entry label if retained as a tenant alias, but it must map onto a `LearnSession`.
- Any inherited `/teach/*` routes are deprecated compatibility aliases only and must be normalized to `/learn/*`.

---

## §0C R11.2 Runtime Truth, Learning Semantics, Later-Use Completion, and Production Hardening

### §0C.1 Canonical LearnSession schema (authoritative override)

```ts
// packages/contracts/src/learning/learn-session.ts
import { z } from "zod";

export const LearningEntryModeSchema = z.enum([
  "demonstrating_skill",
  "coaching_agent",
  "autonomous_agent_practice",
  "improving_existing_skill",
  "import_skill",
]);

export const ObservationModeSchema = z.enum([
  "none",
  "inside_agent",
  "outside_agent",
  "mixed",
]);

export const BuildPolicySchema = z.enum([
  "ask_before_build",
  "build_then_review",
  "private_auto_install",
]);

export const LearnExecutionModeSchema = z.enum([
  "strict",
  "guided",
  "adaptive",
]);

export const LearnSessionStateSchema = z.enum([
  "idle",
  "armed",
  "capturing",
  "paused",
  "stopped",
  "reviewing",
  "awaiting_questions",
  "drafting_proposal",
  "testing_proposal",
  "ready_for_review",
  "installing_private",
  "installed_private",
  "approved",
  "rejected",
  "cancelled",
  "quarantined",
]);

export const LearnTargetKindSchema = z.enum([
  "new_skill",
  "existing_skill",
  "imported_skill",
]);

export const LearnSessionSchema = z.object({
  learn_session_id: z.string().uuid(),
  entry_mode: LearningEntryModeSchema,
  observation_mode: ObservationModeSchema.default("none"),
  build_policy: BuildPolicySchema.default("build_then_review"),
  execution_mode: LearnExecutionModeSchema.default("guided"),
  target_kind: LearnTargetKindSchema.default("new_skill"),
  state: LearnSessionStateSchema,
  actor_mode: z.enum(["user_driven", "agent_driven", "mixed"]).default("mixed"),
  project_id: z.string().max(120).optional(),
  room_id: z.string().max(160).optional(),
  target_skill_id: z.string().max(160).optional(),
  target_app_family: z.string().max(80).optional(),
  target_app_whitelist: z.array(z.string().max(160)).default([]),
  active_trace_ids: z.array(z.string().uuid()).default([]),
  active_observation_batch_ids: z.array(z.string().uuid()).default([]),
  proposal_id: z.string().uuid().optional(),
  interpretation_id: z.string().uuid().optional(),
  validation_run_id: z.string().uuid().optional(),
  ability_id: z.string().max(160).optional(),
  start_boundary_ref: z.string().max(240).optional(),
  stop_boundary_ref: z.string().max(240).optional(),
  test_policy_ref: z.string().max(240).optional(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0C.1A LearnSession valid state transitions (authoritative)

| From state | Valid to states |
|---|---|
| `idle` | `armed`, `cancelled` |
| `armed` | `capturing`, `cancelled` |
| `capturing` | `paused`, `stopped`, `cancelled` |
| `paused` | `capturing` (resume), `stopped`, `cancelled` |
| `stopped` | `reviewing`, `cancelled` |
| `reviewing` | `awaiting_questions`, `drafting_proposal`, `cancelled` |
| `awaiting_questions` | `reviewing`, `drafting_proposal`, `cancelled` |
| `drafting_proposal` | `testing_proposal`, `ready_for_review`, `cancelled` |
| `testing_proposal` | `ready_for_review`, `drafting_proposal` (revision), `cancelled`, `quarantined` |
| `ready_for_review` | `installing_private`, `approved`, `rejected`, `drafting_proposal` (revision) |
| `installing_private` | `installed_private`, `quarantined` |
| `installed_private` | `approved`, `quarantined` |
| `approved` | `quarantined` |
| `rejected` | (terminal) |
| `cancelled` | (terminal) |
| `quarantined` | `drafting_proposal` (revision), `cancelled` |

Any transition not in this table must be rejected with error code `INVALID_LEARN_STATE_TRANSITION`.

**Implementation:**

```ts
// apps/ec-service/src/learning/state-machine.ts
const VALID_TRANSITIONS: Record<string, string[]> = {
  idle: ["armed", "cancelled"],
  armed: ["capturing", "cancelled"],
  capturing: ["paused", "stopped", "cancelled"],
  paused: ["capturing", "stopped", "cancelled"],
  stopped: ["reviewing", "cancelled"],
  reviewing: ["awaiting_questions", "drafting_proposal", "cancelled"],
  awaiting_questions: ["reviewing", "drafting_proposal", "cancelled"],
  drafting_proposal: ["testing_proposal", "ready_for_review", "cancelled"],
  testing_proposal: ["ready_for_review", "drafting_proposal", "cancelled", "quarantined"],
  ready_for_review: ["installing_private", "approved", "rejected", "drafting_proposal"],
  installing_private: ["installed_private", "quarantined"],
  installed_private: ["approved", "quarantined"],
  approved: ["quarantined"],
  rejected: [],
  cancelled: [],
  quarantined: ["drafting_proposal", "cancelled"],
};

export function validateTransition(from: string, to: string): boolean {
  return (VALID_TRANSITIONS[from] ?? []).includes(to);
}
```

---

### §0C.1B Concurrency constraint (authoritative)

At most **one** `LearnSession` per user may be in a non-terminal state at any time. Terminal states are `rejected`, `cancelled`. All other states are non-terminal.

Attempting to create a second active session via `POST /api/learn/sessions` while another session is non-terminal must return:

```json
{ "error": "LEARN_SESSION_ALREADY_ACTIVE", "active_session_id": "<existing_id>" }
```

The user must cancel or complete the existing session before starting a new one.

---

### §0C.2 CreateLearnSession request/response (authoritative override)

```ts
// packages/contracts/src/learning/create-learn-session.ts
import { z } from "zod";
import {
  LearnSessionSchema,
  LearningEntryModeSchema,
  ObservationModeSchema,
  BuildPolicySchema,
  LearnExecutionModeSchema,
  LearnTargetKindSchema,
} from "./learn-session";

export const CreateLearnSessionRequestSchema = z.object({
  entry_mode: LearningEntryModeSchema,
  observation_mode: ObservationModeSchema.default("none"),
  build_policy: BuildPolicySchema.default("build_then_review"),
  execution_mode: LearnExecutionModeSchema.default("guided"),
  target_kind: LearnTargetKindSchema.default("new_skill"),
  project_id: z.string().max(120).optional(),
  room_id: z.string().max(160).optional(),
  target_skill_id: z.string().max(160).optional(),
  target_app_family: z.string().max(80).optional(),
  target_app_whitelist: z.array(z.string().max(160)).default([]),
  ask_before_build: z.boolean().default(false),
  allow_private_install: z.boolean().default(false),
  requested_safe_target_ref: z.string().max(240).optional(),
});

export const CreateLearnSessionResponseSchema = z.object({
  session: LearnSessionSchema,
  ui_mode: z.enum(["overlay", "drawer", "recent-run", "exploratory", "import"]),
  next_action: z.enum([
    "wait_for_first_meaningful_event",
    "show_recent_run_picker",
    "launch_exploration",
    "show_skill_delta_editor",
    "show_import_configurator",
  ]),
});
```

**Capture-scoping rule:**
When `target_app_whitelist` is non-empty and the session uses `outside_agent` or `mixed` observation, EC must ignore observed events whose `app_family`, bundle id, or normalized window target do not match the whitelist.

### §0C.3 Mapping of user-facing learning modes to runtime

| User-facing mode | `entry_mode` | Typical `observation_mode` | Typical `target_kind` |
|---|---|---|---|
| Demonstrating Skill | `demonstrating_skill` | `outside_agent` or `mixed` | `new_skill` |
| Coaching Agent | `coaching_agent` | `inside_agent` or `mixed` | `new_skill` |
| Autonomous Agent Practice | `autonomous_agent_practice` | `inside_agent` | `new_skill` |
| Improving Existing Skill | `improving_existing_skill` | `inside_agent`, `outside_agent`, or `mixed` | `existing_skill` |
| Import Skill | `import_skill` | `none` | `imported_skill` |

### §0C.4 Learn mode semantics (authoritative clarification)

#### Demonstrating Skill
- user intentionally demonstrates a workflow
- outside-agent observation may be enabled
- user can mark start, stop, goal, and meaningful steps
- recommended for exact by-example teaching

#### Coaching Agent
- user gives structure, hints, and example constraints
- the agent fills in parts of the workflow on its own
- suitable for semi-guided learning

#### Autonomous Agent Practice
- user gives goal, constraints, and policy
- the agent iterates toward a verified solution
- suitable for safe, practice-oriented learning

#### Improving Existing Skill
- agent generates a delta against an existing skill
- never edits the canonical promoted skill in place
- requires proposal / review / validation

#### Import Skill
- stages a third-party skill bundle or OpenClaw/AgentSkills-compatible bundle
- compatibility/lint/collision scanning required before install or promotion

### §0C.5 Observation is a capture option, not a top-level mode

Observation of actions must be modeled as an **observation_mode** / capture option, not a separate top-level learning mode.

This means:
- "Watch My Actions" is valid as a toggle within relevant learning modes
- "Observe My Actions" is a deprecated standalone label
- the UI should present observation controls inside the session configuration pane, not as a separate primary mode card

### §0C.6 Canonical Pending Ability UI state mapping

```ts
// packages/contracts/src/learning/pending-ability-ui-state.ts
import { z } from "zod";

export const PendingAbilityUiStateSchema = z.enum([
  "capturing",
  "awaiting_input",
  "drafting",
  "testing",
  "ready_for_review",
  "private_active",
  "revision_required",
]);

export const mapInternalStateToPendingAbilityUiState = (
  learnState: string,
  installLane: string | null,
): z.infer<typeof PendingAbilityUiStateSchema> => {
  if (learnState === "capturing" || learnState === "paused") return "capturing";
  if (learnState === "awaiting_questions") return "awaiting_input";
  if (learnState === "drafting_proposal") return "drafting";
  if (learnState === "testing_proposal") return "testing";
  if (learnState === "ready_for_review") return "ready_for_review";
  if (installLane === "experimental_private" || learnState === "installed_private") return "private_active";
  return "revision_required";
};
```

### §0C.7 LearningRuntimeSnapshot (new canonical runtime truth surface)

This snapshot is the required answer source for:
- what learning mode is being used,
- what session is active,
- what data is being observed,
- what proposal/validation/install state exists,
- and whether the learned ability is actually usable now.

```ts
// packages/contracts/src/learning/runtime-snapshot.ts
import { z } from "zod";
import {
  LearningEntryModeSchema,
  ObservationModeSchema,
  LearnSessionStateSchema,
  BuildPolicySchema,
} from "./learn-session";

export const LearningRuntimeSnapshotSchema = z.object({
  learn_session_id: z.string().uuid(),
  entry_mode: LearningEntryModeSchema,
  observation_mode: ObservationModeSchema,
  build_policy: BuildPolicySchema,
  state: LearnSessionStateSchema,
  observed_data_classes: z.array(z.string().max(80)).default([]),
  active_boundary_ref: z.string().max(240).optional(),
  interpretation_id: z.string().uuid().optional(),
  proposal_id: z.string().uuid().optional(),
  validation_run_id: z.string().uuid().optional(),
  install_lane: z.enum([
    "pending",
    "experimental_private",
    "approved_workspace",
    "shared_promoted",
    "quarantined",
  ]).optional(),
  ability_id: z.string().max(160).optional(),
  ability_usable_now: z.boolean().optional(),
  reason_unusable: z.string().max(240).optional(),
  latest_receipt_ids: z.array(z.string().uuid()).default([]),
  snapshot_as_of: z.string().datetime(),
  schema_version: z.literal(1),
});
```

Canonical storage:
```text
ELNOR_MEMORY/system/learning/runtime/current.json
```

### §0C.8 ObservationScopeRuntime (new canonical visibility surface)

```ts
// packages/contracts/src/learning/observation-scope-runtime.ts
import { z } from "zod";

export const ObservationScopeRuntimeSchema = z.object({
  learn_session_id: z.string().uuid(),
  adapters_active: z.array(z.string().max(80)).default([]),
  data_classes_observed: z.array(z.string().max(80)).default([]),
  redaction_modes: z.record(z.string(), z.string()).default({}),
  scope: z.enum(["inside_agent", "outside_agent", "mixed", "none"]),
  target_app_whitelist: z.array(z.string().max(160)).default([]),
  off_target_events_dropped: z.number().int().min(0).default(0),
  started_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

Canonical storage:
```text
ELNOR_MEMORY/system/learning/runtime/observation_scope_current.json
```

### §0C.9 SemanticWorkflowInterpretation (new canonical meaning object)

This is the required answer source for:
- what meaning was attached to the workflow,
- when to use the learned ability,
- when not to use it,
- what goal/constraints/success conditions were ultimately confirmed.

```ts
// packages/contracts/src/learning/semantic-workflow-interpretation.ts
import { z } from "zod";

export const SemanticWorkflowInterpretationSchema = z.object({
  interpretation_id: z.string().uuid(),
  learn_session_id: z.string().uuid(),
  proposal_id: z.string().uuid().optional(),
  title: z.string().max(160),
  goal_summary: z.string().max(600),
  use_conditions: z.array(z.string().max(240)).default([]),
  non_use_conditions: z.array(z.string().max(240)).default([]),
  project_scope: z.enum(["global", "project_scoped", "unclear"]),
  preferred_control_surfaces: z.array(z.string().max(80)).default([]),
  success_conditions: z.array(z.string().max(240)).default([]),
  user_confirmed: z.boolean().default(false),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

Canonical storage:
```text
ELNOR_MEMORY/system/learning/interpretations/<interpretation_id>.json
```

### §0C.9A Execution model for learned skills (authoritative)

#### Rule 1 — Internal representation vs runtime execution

The `WorkflowGraphSchema` (§0A.15) and `SemanticWorkflowInterpretationSchema` (§0C.9) are **internal review, validation, and repair artifacts only**. They are never serialized into OpenClaw's context window as an execution plan.

Learned skills execute through the identical `SKILL.md` + OpenClaw ReAct loop as hand-authored skills. There is no separate "learned skill executor," no FSM graph runner, and no node-by-node step engine.

The relationship between the internal graph and the runtime artifact is:

```
WorkflowGraph (IR)
  → SemanticWorkflowInterpretation (meaning + checkpoints)
    → SKILL.md generation (compiled natural-language guidance)
      → OpenClaw ReAct loop (runtime execution)
```

This is a compiler pipeline. The IR is not the emitted code.

#### Rule 2 — Checkpoint-oriented SKILL.md generation policy

All SKILL.md content generated by the learning pipeline must be written as **semantic checkpoints and verification goals**, never as rigid action sequences.

**Forbidden output patterns:**
```markdown

### §0C.9B Checkpoint progress display in Q (authoritative UI contract)

When a skill with checkpoints is executing (either in a LearnSession rehearsal or in normal later-use dispatch), Q must display checkpoint progress in the session timeline or dispatch detail view.

#### Required UI states per checkpoint

| Checkpoint status | Visual indicator | Detail |
|-------------------|-----------------|--------|
| Not yet reached | Grey circle / empty | Checkpoint goal text shown |
| Reached | Green filled circle / checkmark | Evidence summary shown on expand |
| Skipped | Yellow circle / skip icon | Reason shown on expand |
| Failed | Red circle / X | Reason + evidence shown on expand |

#### Layout

Within the session detail drawer or dispatch detail panel:

```
┌─────────────────────────────────────────┐
│ Checkpoint Progress                      │
│                                          │
│ ● Export dialog is open          ✓       │
│   Evidence: "Export dialog visible..."   │
│                                          │
│ ● PDF format is selected         ✓       │
│   Evidence: "PDF option active..."       │
│                                          │
│ ○ Export confirmed               —       │
│   (not yet reached)                      │
│                                          │
│ ○ File exists at destination     —       │
│   (not yet reached)                      │
└─────────────────────────────────────────┘
```

#### Implementation

```ts
// apps/q-frontend/src/features/learn/CheckpointProgressView.tsx

interface CheckpointProgressProps {
  checkpoints: Array<{
    checkpoint_id: string;
    goal: string;
    success_condition: string;
    order_hint?: number;
  }>;
  receipts: Array<{
    checkpoint_id: string;
    status: "reached" | "skipped" | "failed";
    evidence: string;
    reason?: string;
    created_at: string;
  }>;
}

// Component renders checkpoints sorted by order_hint, with receipt
// status overlaid. Checkpoints without receipts show as "not yet reached".
// Expand/collapse per checkpoint to show evidence/reason.
```

#### Data source

Q fetches checkpoint receipts via:
```http
GET /api/learn/sessions/:sessionId/checkpoint-receipts
GET /api/dispatches/:dispatchId/checkpoint-receipts
```

These routes return filtered views of `ELNOR_MEMORY/system/learning/checkpoint_receipts.jsonl` for the given session or dispatch.

---

---

### §0C.9C Wrapper and script single-writer enforcement (coding-agent directive)

#### Authoritative rule

OpenClaw skills, Python wrappers, Bash scripts, AppleScript wrappers, and any other local execution surface are **strictly forbidden** from writing to local configuration files, JSON state files, YAML files, or any persistent storage outside of:

- transient working copies (temporary files that are cleaned up after use),
- stdout/stderr output consumed by the calling agent,
- and EC-routed command queue submissions.

All persistent configuration changes must be submitted as a `ConfigurationIntentSchema` payload (defined in §0.2G at `packages/contracts/src/common/configuration-intent.ts`) to the EC command queue. EC validates and writes the configuration to `ELNOR_MEMORY/system/skills/config/` and projects it to the workspace if needed.

#### Explicit prohibition for coding agents

The following patterns are **forbidden** in any wrapper, script, or skill implementation:

```python

### §0C.10 ValidationRun and ValidationStepResult (new canonical testing truth)

```ts
// packages/contracts/src/learning/validation-run.ts
import { z } from "zod";

export const ValidationRunSchema = z.object({
  validation_run_id: z.string().uuid(),
  proposal_id: z.string().uuid(),
  run_kind: z.enum([
    "lint",
    "trigger",
    "dry_run",
    "canary",
    "adapter_check",
    "dependency_check",
    "rehearsal",
  ]),
  started_at: z.string().datetime(),
  completed_at: z.string().datetime().optional(),
  overall_result: z.enum(["passed", "failed", "partial", "cancelled"]),
  result_refs: z.array(z.string().max(240)).default([]),
  schema_version: z.literal(1),
});

export const ValidationStepResultSchema = z.object({
  validation_run_id: z.string().uuid(),
  test_case_id: z.string().max(160),
  label: z.string().max(240),
  result: z.enum(["passed", "failed", "skipped"]),
  details: z.string().max(1000).optional(),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

Canonical storage:
```text
ELNOR_MEMORY/system/learning/validation_runs.jsonl
ELNOR_MEMORY/system/learning/validation_step_results.jsonl
```

### §0C.10A Proposal, clarification, and dependency contracts (authoritative)

```ts
// packages/contracts/src/learning/proposal.ts
import { z } from "zod";

export const LearningQuestionSchema = z.object({
  question_id: z.string().uuid(),
  proposal_id: z.string().uuid(),
  field: z.enum([
    "name",
    "goal_summary",
    "use_conditions",
    "non_use_conditions",
    "project_scope",
    "preferred_surface",
    "package_assignment",
    "success_condition",
  ]),
  prompt: z.string().max(400),
  required: z.boolean().default(true),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const LearningAnswerSchema = z.object({
  question_id: z.string().uuid(),
  proposal_id: z.string().uuid(),
  answer: z.string().max(2000),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const LearningDocumentDependencySchema = z.object({
  proposal_id: z.string().uuid(),
  dependency_kind: z.enum([
    "support_pack",
    "app_manual",
    "existing_skill",
    "capability_manifest",
    "test_artifact",
  ]),
  dependency_ref: z.string().max(240),
  required: z.boolean().default(true),
  schema_version: z.literal(1),
});

export const SkillBundleProposalStateSchema = z.enum([
  "draft",
  "awaiting_questions",
  "testing",
  "ready_for_review",
  "installing_private",
  "installed_private",
  "approved",
  "rejected",
  "cancelled",
  "quarantined",
]);

export const SkillBundleProposalSchema = z.object({
  proposal_id: z.string().uuid(),
  learn_session_id: z.string().uuid().optional(),
  interpretation_id: z.string().uuid().optional(),
  source_trace_ids: z.array(z.string().uuid()).min(1),
  title: z.string().max(160),
  proposed_skill_slug: z.string().max(120),
  trigger_phrases: z.array(z.string().max(120)).default([]),
  negative_triggers: z.array(z.string().max(120)).default([]),
  draft_skill_path: z.string().max(240),
  draft_manifest_path: z.string().max(240),
  draft_graph_ref: z.string().max(240).optional(),
  proposal_state: SkillBundleProposalStateSchema,
  install_lane: z.enum([
    "pending",
    "experimental_private",
    "approved_workspace",
    "shared_promoted",
    "quarantined",
  ]).default("pending"),
  ability_id: z.string().max(160).optional(),
  primary_capability_id: z.string().max(160).optional(),
  support_pack_refs: z.array(z.string().max(240)).default([]),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```
```

### §0C.11 Ability availability and later-use lookup (authoritative paths)

Canonical ability availability artifact:
```text
ELNOR_MEMORY/system/abilities/availability_current.json
```

Canonical ability lookup routes:
```http
GET  /api/abilities/availability
GET  /api/abilities/:abilityId/availability
POST /api/abilities/lookup
POST /api/abilities/explain-match
```

Canonical lookup schemas:
```ts
// packages/contracts/src/abilities/lookup.ts
import { z } from "zod";

export const AbilityLookupQuerySchema = z.object({
  user_query: z.string().max(500),
  project_id: z.string().max(120).optional(),
  room_id: z.string().max(160).optional(),
  install_lanes_allowed: z.array(z.enum([
    "experimental_private",
    "approved_workspace",
    "shared_promoted",
  ])).default(["approved_workspace", "shared_promoted"]),
  include_private_owned_by_user: z.boolean().default(true),
  schema_version: z.literal(1),
});

export const AbilityLookupResultSchema = z.object({
  matches: z.array(z.object({
    ability_id: z.string().max(160),
    score: z.number(),
    install_lane: z.string(),
    usable_now: z.boolean(),
    reason_unusable: z.string().optional(),
  })),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0C.11A Ability availability snapshot and Skills catalog (authoritative)

```ts
// packages/contracts/src/abilities/availability.ts
import { z } from "zod";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const AbilityAvailabilityEntrySchema = z.object({
  ability_id: z.string().max(160),
  title: z.string().max(200),
  family: z.string().max(80),
  source: z.enum(["learned", "imported", "native", "promoted"]),
  skill_slug: z.string().max(120).optional(),
  primary_capability_id: z.string().max(160).optional(),
  install_lane: z.enum([
    "experimental_private",
    "approved_workspace",
    "shared_promoted",
    "quarantined",
  ]),
  enabled: z.boolean(),
  usable_now: z.boolean(),
  reason_unusable: z.string().max(240).optional(),
  trigger_phrases: z.array(z.string().max(120)).default([]),
  negative_triggers: z.array(z.string().max(120)).default([]),
  aliases: z.array(z.string().max(120)).default([]),
  project_scopes: z.array(z.string().max(160)).default([]),
  preferred_control_surfaces: z.array(ControlSurfaceSchema).default([]),
  support_pack_refs: z.array(z.string().max(240)).default([]),
  checkpoint_health: z.enum(["strong", "moderate", "weak", "no_data", "quarantined"]).optional(),
  last_validated_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});

export const AbilityAvailabilitySnapshotSchema = z.object({
  abilities: z.array(AbilityAvailabilityEntrySchema),
  snapshot_as_of: z.string().datetime(),
  schema_version: z.literal(1),
});

// packages/contracts/src/abilities/catalog.ts
export const SkillCatalogEntrySchema = z.object({
  ability_id: z.string().max(160),
  title: z.string().max(200),
  family: z.string().max(80),
  source: z.enum(["learned", "imported", "native", "promoted"]),
  install_lane: z.enum([
    "experimental_private",
    "approved_workspace",
    "shared_promoted",
    "quarantined",
  ]),
  enabled: z.boolean(),
  usable_now: z.boolean(),
  reason_unusable: z.string().max(240).optional(),
  tags: z.array(z.string().max(80)).default([]),
  category_ids: z.array(z.string().uuid()).default([]),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const SkillCatalogCurrentSchema = z.object({
  items: z.array(SkillCatalogEntrySchema),
  refreshed_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

Canonical storage:
```text
ELNOR_MEMORY/system/abilities/availability_current.json
ELNOR_MEMORY/system/abilities/catalog_current.json
```
```

### §0C.11B Availability snapshot refresh triggers (authoritative)

`ELNOR_MEMORY/system/abilities/availability_current.json` must be rebuilt whenever any of the following events occur:

| Trigger event | Source |
|---|---|
| `learn.install.completed` | Learning install saga |
| `learn.install.failed` | Learning install saga (mark ability unavailable) |
| Ability activated / deactivated | `POST /api/abilities/:id/activate` or `deactivate` |
| Ability quarantined / unquarantined | `POST /api/abilities/:id/quarantine` or `unquarantine` |
| Ability promoted to shared | `POST /api/abilities/:id/promote-shared` |
| Capability health changed | DOC3 health/quarantine/reprobe system |
| Connector health changed | MCP health monitoring |
| Bridge rebuild completed | `bridge.rebuild.completed` event |
| Project binding changed | Core/DOC7 project resolver |
| Nightly learning job | Nightly scheduler Phase 4 |

**Staleness rule:** If the snapshot is older than the latest event that should have triggered a refresh, Q must show a "refreshing abilities..." indicator. Q must NOT show stale availability data as current.

**Storage path:** `ELNOR_MEMORY/system/abilities/availability_current.json`  
**Writer:** EC only (single-writer rule).

---

### §0C.11C Ability matching algorithm (authoritative)

The `POST /api/abilities/lookup` route must implement the following matching algorithm:

```ts
// apps/ec-service/src/learning/ability-lookup.ts

interface ScoredAbility {
  ability_id: string;
  score: number;
  match_reasons: string[];
  install_lane: string;
  usable_now: boolean;
  reason_unusable?: string;
}

export function scoreAbilityMatch(
  query: string,
  ability: AbilityAvailabilityEntry,
  projectId?: string,
): ScoredAbility {
  let score = 0;
  const reasons: string[] = [];
  const queryLower = query.toLowerCase();
  const queryTokens = queryLower.split(/\s+/);

  // 1. Trigger phrase matching (primary signal, weight 0.4)
  for (const trigger of ability.trigger_phrases) {
    const triggerLower = trigger.toLowerCase();
    if (queryLower.includes(triggerLower) || triggerLower.includes(queryLower)) {
      score += 0.4;
      reasons.push(`trigger_phrase_match: "${trigger}"`);
      break;
    }
    // Partial token overlap
    const triggerTokens = triggerLower.split(/\s+/);
    const overlap = queryTokens.filter(t => triggerTokens.includes(t)).length;
    if (overlap >= 2) {
      score += 0.2;
      reasons.push(`trigger_partial_match: ${overlap} tokens`);
      break;
    }
  }

  // 2. Negative trigger exclusion (hard filter)
  for (const neg of ability.negative_triggers) {
    if (queryLower.includes(neg.toLowerCase())) {
      return {
        ability_id: ability.ability_id,
        score: 0,
        match_reasons: [`negative_trigger_match: "${neg}"`],
        install_lane: ability.install_lane,
        usable_now: ability.usable_now,
        reason_unusable: ability.reason_unusable,
      };
    }
  }

  // 3. Title and family matching (weight 0.2)
  const titleLower = ability.title.toLowerCase();
  const titleOverlap = queryTokens.filter(t => titleLower.includes(t)).length;
  if (titleOverlap >= 2) {
    score += 0.2;
    reasons.push(`title_match: ${titleOverlap} tokens`);
  }

  // 4. Project scope matching (weight 0.15)
  if (projectId && ability.project_scopes.includes(projectId)) {
    score += 0.15;
    reasons.push(`project_scope_match: ${projectId}`);
  } else if (ability.project_scopes.length === 0 || ability.project_scopes.includes("*")) {
    score += 0.05;
    reasons.push("global_scope");
  }

  // 5. Install lane preference (weight 0.1)
  if (ability.install_lane === "shared_promoted") score += 0.1;
  else if (ability.install_lane === "approved_workspace") score += 0.07;
  else if (ability.install_lane === "experimental_private") score += 0.03;

  // 6. Checkpoint health bonus (weight 0.05)
  if (ability.checkpoint_health === "strong") score += 0.05;
  else if (ability.checkpoint_health === "weak") score -= 0.05;

  // 7. Usability gate
  if (!ability.usable_now) {
    score *= 0.5; // Demote but don't exclude — let caller decide
    reasons.push(`usable_now=false: ${ability.reason_unusable}`);
  }

  return {
    ability_id: ability.ability_id,
    score: Math.min(1, Math.max(0, score)),
    match_reasons: reasons,
    install_lane: ability.install_lane,
    usable_now: ability.usable_now,
    reason_unusable: ability.reason_unusable,
  };
}
```

**Score range:** `[0, 1]`. Minimum threshold for inclusion in results: `0.15`. Results returned sorted by score descending, max 10 results.

**DOC10 integration:** DOC10's intent broker calls `POST /api/abilities/lookup` during intent resolution (step 3 of the §0E.11 query order). If a match with `score >= 0.3` and `usable_now = true` is found, DOC10 may route to the learned skill.

---

### §0C.12 Learning receipts (canonical path)

Canonical path:
```text
ELNOR_MEMORY/system/learning/receipts.jsonl
```

Every lifecycle change below must emit a LearningReceipt:
- session created
- observation armed
- observation stopped
- proposal created
- clarification requested
- clarification answered
- validation started
- validation finished
- install requested
- install completed
- bridge rebuild completed
- ability availability updated
- ability activated/deactivated/quarantined

### §0C.13 Learning lifecycle events / saga contract

```ts
// packages/contracts/src/learning/lifecycle-event.ts
import { z } from "zod";

export const LearningLifecycleEventSchema = z.enum([
  "learn.session.started",
  "learn.capture.started",
  "learn.capture.paused",
  "learn.capture.stopped",
  "learn.proposal.created",
  "learn.proposal.questions_requested",
  "learn.validation.started",
  "learn.validation.completed",
  "learn.install.started",
  "learn.install.completed",
  "bridge.rebuild.started",
  "bridge.rebuild.completed",
  "ability.snapshot.updated",
  "ability.route.enabled",
  "ability.quarantined",
]);
```

Minimal saga table (authoritative):
| Command | Emits | Rollback / compensation |
|---|---|---|
| `createLearnSession` | `learn.session.started` | delete empty draft session if immediate failure |
| `stopCapture` | `learn.capture.stopped` | n/a |
| `generateProposal` | `learn.proposal.created` | mark proposal `cancelled` on fatal draft failure |
| `runValidation` | `learn.validation.started`, `learn.validation.completed` | preserve failed results; no silent retry loop |
| `installProposal` | `learn.install.started`, `learn.install.completed` | if manifest projection fails, revert private install and mark proposal `quarantined` |
| `rebuildBridge` | `bridge.rebuild.started`, `bridge.rebuild.completed` | keep previous bridge current; mark degraded if rebuild fails |
| `refreshAvailability` | `ability.snapshot.updated` | preserve previous snapshot and surface stale state |

### §0C.14 Rehearsal Policy replaces heavy sandbox assumptions

R11.2 continues the R11.1 move away from vague heavy "sandbox" expectations and keeps the lighter, more practical **Test / Rehearsal Policy**.

#### Authoritative rule
The system does **not** require a full isolated sandbox for every app.  
Instead, each learning session / proposal may declare a rehearsal policy appropriate to the app family.

```ts
// packages/contracts/src/learning/test-policy.ts
import { z } from "zod";

export const TestPolicyModeSchema = z.enum([
  "read_only",
  "dry_run_only",
  "blank_artifact",
  "copy_artifact",
  "safe_target_only",
  "redirect_send",
  "live_read_safe_write",
  "controlled_live_test",
]);

export const RehearsalSafeTargetSchema = z.object({
  target_kind: z.enum([
    "temp_folder",
    "blank_document",
    "copied_document",
    "blank_project",
    "safe_email_recipient",
    "managed_browser_profile",
    "test_sharepoint_site",
  ]),
  ref: z.string().max(240),
});

export const TestPolicySchema = z.object({
  policy_id: z.string().uuid(),
  mode: TestPolicyModeSchema,
  safe_targets: z.array(RehearsalSafeTargetSchema).default([]),
  forbid_destructive_writes: z.boolean().default(true),
  forbid_external_send: z.boolean().default(true),
  allowed_domains: z.array(z.string().max(120)).default([]),
  max_actions: z.number().int().min(1).max(200).default(40),
  cleanup_required: z.boolean().default(false),
  schema_version: z.literal(1),
});
```

#### Required guidance by app family
- **Word / docs**: prefer blank or copied documents
- **Email**: prefer draft-only or redirect-send to a safe recipient
- **Browser**: prefer managed browser profile and safe domains
- **Westlaw**: allow live read/search with budget/guard rules; no destructive/billable side effects without explicit policy
- **Bitwig / Ableton / DAW-class**: prefer controlled live test with blank project templates and safe output folders; do not require impossible VM-style isolation

### §0C.15 UI naming and page contracts (authoritative)

#### Learn page title
**Ability Learning**

#### Learn page tabs
- Learn New Ability
- Active Capability Learning
- Pending Abilities
- History

#### Learn New Ability cards
Heading:
> Create new ability by:

Cards:
1. Demonstrating Skill
2. Coaching Agent
3. Autonomous Agent Practice
4. Improving Existing Skill
5. Import Skill

#### Status rule
- Anything not yet accepted/promoted into Skills appears in **Pending Abilities**
- Approved/promoted learned abilities appear in **Skills**
- Internal/private install-lane distinctions may exist in backend telemetry but should not require a separate primary user-facing "Experimental" category


### §0C.16 Canonical learning and ability route family (authoritative — R11.2)

All learning, proposal, and ability lifecycle routes live under `/api/learn/` and `/api/abilities/`. No other route prefix is canonical for these concerns.

#### Deprecated route prefixes (do not implement)
- `/api/capabilities/teach/*` — SUPERSEDED by `/api/learn/*`
- `/api/learn/session/*` (singular) — SUPERSEDED by `/api/learn/sessions/*` (plural)
- `/api/skills/mining/*` — MERGED into `/api/learn/*`

Legacy clients hitting deprecated prefixes should receive `308 Permanent Redirect` or an internal canonical alias that preserves method and body for 6 months after deployment, then `410 Gone`.

#### Session lifecycle routes

```http
POST   /api/learn/sessions                              # Create session
GET    /api/learn/sessions                              # List sessions (paginated)
GET    /api/learn/sessions/:sessionId/detail             # Session detail
POST   /api/learn/sessions/:sessionId/start              # Arm → Capturing
POST   /api/learn/sessions/:sessionId/pause              # Capturing → Paused
POST   /api/learn/sessions/:sessionId/resume             # Paused → Capturing
POST   /api/learn/sessions/:sessionId/stop               # Capturing/Paused → Stopped
POST   /api/learn/sessions/:sessionId/cancel             # Any non-terminal → Cancelled
POST   /api/learn/sessions/:sessionId/mark-goal          # Record goal marker during capture
POST   /api/learn/sessions/:sessionId/mark-step          # Record step marker during capture
POST   /api/learn/sessions/:sessionId/trim               # Adjust start/end boundaries
POST   /api/learn/sessions/:sessionId/answer-questions    # Answer clarification questions
POST   /api/learn/sessions/:sessionId/set-test-policy     # Set rehearsal policy
POST   /api/learn/sessions/:sessionId/generate-proposal   # Stopped/Reviewing → Drafting
GET    /api/learn/sessions/:sessionId/events              # SSE stream of lifecycle events
GET    /api/learn/sessions/:sessionId/observation-scope    # Current observation state
GET    /api/learn/sessions/:sessionId/checkpoint-receipts  # Checkpoint verification receipts
DELETE /api/learn/sessions/:sessionId                     # Cleanup terminated session
```

#### Proposal lifecycle routes

```http
GET    /api/learn/proposals                              # List proposals (paginated)
GET    /api/learn/proposals/:proposalId/detail            # Proposal detail + interpretation
GET    /api/learn/proposals/:proposalId/validation-runs   # Validation results
POST   /api/learn/proposals/:proposalId/revise            # Request revision → Drafting
POST   /api/learn/proposals/:proposalId/validate          # Start validation run
POST   /api/learn/proposals/:proposalId/install           # Install (private or approved)
POST   /api/learn/proposals/:proposalId/approve           # Approve for workspace/shared
POST   /api/learn/proposals/:proposalId/reject            # Reject proposal
```

#### Ability lifecycle routes

```http
GET    /api/abilities/availability                       # Full availability snapshot
GET    /api/abilities/:abilityId/availability             # Single ability availability
POST   /api/abilities/lookup                              # Query for matching abilities
POST   /api/abilities/explain-match                       # Explain why ability matched/didn't
POST   /api/abilities/:abilityId/activate                 # Enable ability for routing
POST   /api/abilities/:abilityId/deactivate               # Disable ability for routing
POST   /api/abilities/:abilityId/quarantine               # Quarantine (removes from routing)
POST   /api/abilities/:abilityId/unquarantine             # Restore from quarantine
POST   /api/abilities/:abilityId/promote-shared           # Promote from private to shared
```

#### Learning history and receipts

```http
GET    /api/learn/history                                # Paginated history of all sessions
GET    /api/learn/receipts                               # Paginated learning receipts
GET    /api/dispatches/:dispatchId/checkpoint-receipts    # Checkpoint receipts for dispatch
```

#### Runtime truth

```http
GET    /api/learn/runtime/current                        # Current LearningRuntimeSnapshot
```

#### Bridge tool

```http
POST   /api/bridge/verify-checkpoint                     # Voluntary checkpoint verification
POST   /api/config/intents                               # Configuration intent submission
```

---

### §0C.16A Route request/response contracts (authoritative)

All routes return `ApiErrorEnvelopeSchema` on failure. Success responses:

```ts
// packages/contracts/src/learning/route-contracts.ts
import { z } from "zod";
import { LearnSessionSchema } from "./learn-session";
import { LearningRuntimeSnapshotSchema } from "./runtime-snapshot";
import { ObservationScopeRuntimeSchema } from "./observation-scope-runtime";
import { SemanticWorkflowInterpretationSchema } from "./semantic-workflow-interpretation";
import { ValidationRunSchema, ValidationStepResultSchema } from "./validation-run";
import { TestPolicySchema } from "./test-policy";
import { SkillBundleProposalSchema } from "../skills/proposal";
import { LearningReceiptSchema } from "./receipts";
import { CheckpointVerificationReceiptSchema } from "./checkpoint-receipt";
import { AbilityLookupResultSchema } from "../abilities/lookup";

// --- Session lifecycle responses ---

export const SessionActionResponseSchema = z.object({
  session: LearnSessionSchema,
});

export const StartSessionResponseSchema = z.object({
  session: LearnSessionSchema,
  observation_active: z.boolean(),
  adapters_armed: z.array(z.string().max(80)).default([]),
});

export const StopSessionResponseSchema = z.object({
  session: LearnSessionSchema,
  captured_event_count: z.number().int().min(0),
  captured_trace_ids: z.array(z.string().uuid()).default([]),
  next_action: z.enum(["review_captured", "generate_proposal", "cancel"]),
});

export const MarkGoalRequestSchema = z.object({
  goal_description: z.string().max(400),
  at_event_id: z.string().uuid().optional(),
});

export const MarkStepRequestSchema = z.object({
  step_label: z.string().max(240),
  step_kind_hint: z.enum([
    "action", "guard", "verification", "decision", "other"
  ]).default("action"),
  at_event_id: z.string().uuid().optional(),
});

export const TrimRequestSchema = z.object({
  trim_kind: z.enum(["trim_start", "trim_end"]),
  to_event_id: z.string().uuid(),
});

export const AnswerQuestionsRequestSchema = z.object({
  answers: z.array(z.object({
    question_id: z.string().max(80),
    answer_text: z.string().max(2000),
  })).min(1),
});

export const AnswerQuestionsResponseSchema = z.object({
  session: LearnSessionSchema,
  remaining_questions: z.number().int().min(0),
  next_action: z.enum(["more_questions", "generate_proposal", "ready_for_review"]),
});

export const SetTestPolicyRequestSchema = z.object({
  test_policy: TestPolicySchema,
});

export const GenerateProposalResponseSchema = z.object({
  session: LearnSessionSchema,
  proposal_id: z.string().uuid(),
});

// --- Proposal lifecycle responses ---

export const ProposalDetailResponseSchema = z.object({
  proposal: SkillBundleProposalSchema,
  interpretation: SemanticWorkflowInterpretationSchema.optional(),
  checkpoint_lint_report: z.unknown().optional(),
  questions: z.array(z.object({
    question_id: z.string().max(80),
    question_text: z.string().max(400),
    answered: z.boolean(),
    answer_text: z.string().max(2000).optional(),
  })).default([]),
});

export const ProposalReviseRequestSchema = z.object({
  revision_instructions: z.string().max(2000),
  edited_fields: z.record(z.string(), z.unknown()).default({}),
});

export const ProposalInstallRequestSchema = z.object({
  install_lane: z.enum(["experimental_private", "approved_workspace"]),
});

export const ProposalInstallResponseSchema = z.object({
  proposal: SkillBundleProposalSchema,
  ability_id: z.string().max(160),
  saga_id: z.string().uuid(),
});

export const ValidationRunsResponseSchema = z.object({
  runs: z.array(ValidationRunSchema),
  steps: z.array(ValidationStepResultSchema),
});

// --- Ability lifecycle responses ---

export const AbilityAvailabilityResponseSchema = z.object({
  abilities: z.array(z.object({
    ability_id: z.string().max(160),
    title: z.string().max(200),
    family: z.string().max(80),
    install_lane: z.string(),
    enabled: z.boolean(),
    usable_now: z.boolean(),
    reason_unusable: z.string().max(240).optional(),
    checkpoint_health: z.enum(["strong", "moderate", "weak", "no_data", "quarantined"]).optional(),
    source: z.enum(["learned", "imported", "native", "promoted"]),
  })),
  snapshot_as_of: z.string().datetime(),
  schema_version: z.literal(1),
});

export const AbilityExplainMatchRequestSchema = z.object({
  user_query: z.string().max(500),
  ability_id: z.string().max(160),
  project_id: z.string().max(120).optional(),
});

export const AbilityExplainMatchResponseSchema = z.object({
  ability_id: z.string().max(160),
  matched: z.boolean(),
  score: z.number().min(0).max(1),
  match_reasons: z.array(z.string().max(240)).default([]),
  rejection_reasons: z.array(z.string().max(240)).default([]),
});

// --- History and receipts ---

export const LearnHistoryResponseSchema = z.object({
  items: z.array(z.object({
    learn_session_id: z.string().uuid(),
    entry_mode: z.string(),
    state: z.string(),
    title: z.string().max(200).optional(),
    created_at: z.string().datetime(),
    completed_at: z.string().datetime().optional(),
    proposal_id: z.string().uuid().optional(),
    ability_id: z.string().max(160).optional(),
    outcome: z.enum(["approved", "rejected", "cancelled", "in_progress", "quarantined"]).optional(),
  })),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
});

export const LearnReceiptsResponseSchema = z.object({
  receipts: z.array(LearningReceiptSchema),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
});

// --- Events SSE stream ---

export const LearnSessionEventSchema = z.object({
  event_kind: z.string().max(80),
  session_id: z.string().uuid(),
  payload: z.record(z.string(), z.unknown()).default({}),
  timestamp: z.string().datetime(),
});
// Delivered via SSE on GET /api/learn/sessions/:sessionId/events
// Client subscribes on session creation, unsubscribes on terminal state.
```

**Route-to-response mapping:**

| Route | Success response schema |
|---|---|
| `POST /api/learn/sessions` | `CreateLearnSessionResponseSchema` (§0C.2) |
| `GET /api/learn/sessions` | `{ sessions: LearnSessionSchema[], total, page, page_size }` |
| `GET .../detail` | `LearnSessionSchema` |
| `POST .../start` | `StartSessionResponseSchema` |
| `POST .../pause` | `SessionActionResponseSchema` |
| `POST .../resume` | `SessionActionResponseSchema` |
| `POST .../stop` | `StopSessionResponseSchema` |
| `POST .../cancel` | `SessionActionResponseSchema` |
| `POST .../mark-goal` | `SessionActionResponseSchema` |
| `POST .../mark-step` | `SessionActionResponseSchema` |
| `POST .../trim` | `SessionActionResponseSchema` |
| `POST .../answer-questions` | `AnswerQuestionsResponseSchema` |
| `POST .../set-test-policy` | `SessionActionResponseSchema` |
| `POST .../generate-proposal` | `GenerateProposalResponseSchema` |
| `GET .../events` | SSE stream of `LearnSessionEventSchema` |
| `GET .../observation-scope` | `ObservationScopeRuntimeSchema` |
| `GET .../checkpoint-receipts` | `{ receipts: CheckpointVerificationReceiptSchema[] }` |
| `DELETE .../sessions/:id` | `{ deleted: true }` |
| `GET /api/learn/proposals` | `{ proposals: SkillBundleProposalSchema[], total, page, page_size }` |
| `GET .../proposals/:id/detail` | `ProposalDetailResponseSchema` |
| `GET .../validation-runs` | `ValidationRunsResponseSchema` |
| `POST .../revise` | `ProposalDetailResponseSchema` |
| `POST .../validate` | `{ validation_run: ValidationRunSchema }` |
| `POST .../install` | `ProposalInstallResponseSchema` |
| `POST .../approve` | `ProposalDetailResponseSchema` |
| `POST .../reject` | `ProposalDetailResponseSchema` |
| `GET /api/abilities/availability` | `AbilityAvailabilityResponseSchema` |
| `GET /api/abilities/:id/availability` | Single ability from snapshot |
| `POST /api/abilities/lookup` | `AbilityLookupResultSchema` (§0C.11) |
| `POST /api/abilities/explain-match` | `AbilityExplainMatchResponseSchema` |
| `POST /api/abilities/:id/activate` | `{ ability_id, enabled: true }` |
| `POST /api/abilities/:id/deactivate` | `{ ability_id, enabled: false }` |
| `POST /api/abilities/:id/quarantine` | `{ ability_id, quarantined: true, reason }` |
| `POST /api/abilities/:id/unquarantine` | `{ ability_id, quarantined: false }` |
| `POST /api/abilities/:id/promote-shared` | `{ ability_id, install_lane: "shared_promoted" }` |
| `GET /api/learn/history` | `LearnHistoryResponseSchema` |
| `GET /api/learn/receipts` | `LearnReceiptsResponseSchema` |
| `GET /api/learn/runtime/current` | `LearningRuntimeSnapshotSchema` |

---

### §0C.16B Learning error codes (authoritative)

| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
| `LEARN_SESSION_ALREADY_ACTIVE` | 409 | Only one non-terminal session per user | No — cancel or complete first |
| `INVALID_LEARN_STATE_TRANSITION` | 400 | Transition not in §0C.1A table | No |
| `LEARN_SESSION_NOT_FOUND` | 404 | Session ID does not exist | No |
| `PROPOSAL_NOT_FOUND` | 404 | Proposal ID does not exist | No |
| `ABILITY_NOT_FOUND` | 404 | Ability ID does not exist | No |
| `VALIDATION_IN_PROGRESS` | 409 | Cannot install while validating | Yes — wait for completion |
| `INSUFFICIENT_TRACES` | 400 | Below `min_successful_traces` for proposal | No — capture more |
| `ADAPTER_UNAVAILABLE` | 503 | Required observation adapter unhealthy | Yes — retry when healthy |
| `INSTALL_LANE_DENIED` | 403 | Policy prevents this install lane | No |
| `BRIDGE_REBUILD_FAILED` | 500 | Post-install bridge rebuild failed | Yes |
| `TEST_POLICY_VIOLATION` | 400 | Action violates rehearsal policy | No |
| `CHECKPOINT_LINT_FAILED` | 400 | Proposal checkpoints failed lint (§0C.9A Rule 6) | No — revise first |
| `SESSION_TIMEOUT` | 408 | Session exceeded TTL in current state | No — start new session |

---

### §0C.16C Real-time learning event stream (authoritative)

Learning sessions are long-running (minutes to hours for observation). Q must receive real-time updates without polling.

**Contract:** `GET /api/learn/sessions/:sessionId/events` returns a Server-Sent Events (SSE) stream.

**Event format:**
```
event: learn.capture.started
data: {"session_id":"...","timestamp":"...","payload":{"adapter_count":3}}

event: checkpoint.reached
data: {"session_id":"...","checkpoint_id":"cp-01","evidence":"Export dialog visible","timestamp":"..."}

event: learn.proposal.created
data: {"session_id":"...","proposal_id":"...","timestamp":"..."}
```

**Event kinds delivered:** All values from `LearningLifecycleEventSchema` (§0C.13) plus `checkpoint.reached`, `checkpoint.skipped`, `checkpoint.failed`.

**Client behavior:**
- Q subscribes on session creation or when opening a session detail view.
- Q unsubscribes when the session reaches a terminal state or the view is closed.
- If the connection drops, Q reconnects with `Last-Event-ID` header for gap recovery.

**Server behavior:**
- EC emits events as they occur during session lifecycle.
- Server sends keepalive comments (`:keepalive`) every 30 seconds.
- Stream closes when session reaches a terminal state.

---

### §0C.16D Learning artifact TTL and cleanup (authoritative)

| Artifact | TTL rule | Cleanup action |
|---|---|---|
| `LearnSession` in `armed` state | Auto-cancel after 1 hour | Emit `learn.session.cancelled` receipt with reason `"session_timeout"` |
| `LearnSession` in `capturing` state | Auto-pause after 4 hours | Emit `learn.capture.paused` receipt with reason `"capture_timeout"` |
| `LearnSession` in terminal state | Archive after 30 days | Move to `ELNOR_MEMORY/system/learning/archived/sessions/` |
| Raw observation events | Purge 7 days after session terminal | Delete from `ELNOR_MEMORY/system/learning/observations/` unless flagged `retain=true` |
| Proposals in `cancelled` or `rejected` | Archive after 14 days | Move to `ELNOR_MEMORY/system/learning/archived/proposals/` |
| Validation run results | Retain with proposal | Archive with proposal |
| Checkpoint receipts | Retain indefinitely | Subject to nightly compaction (aggregate counts, keep latest 100 per ability) |
| Learning receipts | Retain indefinitely | Subject to nightly compaction |
| `LearningRuntimeSnapshot` | Overwritten on each session change | Previous values not retained (current state only) |

**Nightly job integration:** TTL enforcement runs as part of the nightly scheduler Phase 1 (memory maintenance). The cleanup job must never delete data that is still referenced by a non-terminal session.

---

### §0C.16E Learn read, list, detail, and live-event contracts (authoritative)

**Learn read/list/detail/event response schemas**
```ts
// packages/contracts/src/learning/read-models.ts
import { z } from "zod";

export const LearnObservationScopeResponseSchema = z.object({
  session_id: z.string().uuid(),
  observation_mode: z.enum(["none", "watch_actions", "watch_with_audio", "watch_with_notes"]),
  capture_state: z.enum(["idle", "capturing", "paused", "stopped"]),
  active_runtime: z.enum(["none", "openclaw_native", "bridge_observer"]).default("none"),
  started_at: z.string().datetime().optional(),
  stopped_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});

export const LearnCheckpointReceiptsResponseSchema = z.object({
  receipts: z.array(CheckpointVerificationReceiptSchema).default([]),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
  schema_version: z.literal(1),
});

export const LearnProposalsListResponseSchema = z.object({
  proposals: z.array(SkillBundleProposalSchema).default([]),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
  schema_version: z.literal(1),
});

export const ValidationRunsListResponseSchema = z.object({
  runs: z.array(ValidationRunSchema).default([]),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
  schema_version: z.literal(1),
});

export const LearnSessionEventEnvelopeSchema = z.object({
  event: z.string().max(80),
  data: LearnSessionEventSchema,
  schema_version: z.literal(1),
});
```

**Routes**
```http
GET /api/learn/sessions
GET /api/learn/sessions/:sessionId/detail
GET /api/learn/sessions/:sessionId/events
GET /api/learn/sessions/:sessionId/observation-scope
GET /api/learn/sessions/:sessionId/checkpoint-receipts
GET /api/learn/proposals
GET /api/learn/proposals/:proposalId/detail
GET /api/learn/proposals/:proposalId/validation-runs
GET /api/learn/history
GET /api/learn/receipts
```

**Route request / response map**
| Route | Request schema | Success response |
|---|---|---|
| `GET /api/learn/sessions` | none | `LearnSessionsListResponseSchema` |
| `GET /api/learn/sessions/:sessionId/detail` | none | `LearnSessionDetailResponseSchema` |
| `GET /api/learn/sessions/:sessionId/events` | none | SSE stream of `LearnSessionEventEnvelopeSchema` |
| `GET /api/learn/sessions/:sessionId/observation-scope` | none | `LearnObservationScopeResponseSchema` |
| `GET /api/learn/sessions/:sessionId/checkpoint-receipts` | none | `LearnCheckpointReceiptsResponseSchema` |
| `GET /api/learn/proposals` | none | `LearnProposalsListResponseSchema` |
| `GET /api/learn/proposals/:proposalId/detail` | none | `ProposalDetailResponseSchema` |
| `GET /api/learn/proposals/:proposalId/validation-runs` | none | `ValidationRunsListResponseSchema` |
| `GET /api/learn/history` | none | `LearnHistoryResponseSchema` |
| `GET /api/learn/receipts` | none | `LearnReceiptsResponseSchema` |

**Error / envelope rule**
All non-SSE routes above must use the same top-level success/envelope convention already used elsewhere in DOC3. On failure they must return `ApiErrorEnvelopeSchema`; no route may invent an ad hoc error payload for read/list/detail/event surfaces.

**SSE rule**
`GET /api/learn/sessions/:sessionId/events` is the only required first-wave live event stream for the Learn UI. Clients must:
- subscribe after session creation or detail open,
- reconnect on transient disconnect,
- stop listening when the session enters a terminal state,
- and treat `event = "error"` as a visible degraded banner, not as silent log-only behavior.

### §0C.17 Required backend and frontend modules (R11.2 authoritative)

#### Shared contracts
```text
packages/contracts/src/learning/learn-session.ts
packages/contracts/src/learning/create-learn-session.ts
packages/contracts/src/learning/runtime-snapshot.ts
packages/contracts/src/learning/observation-scope-runtime.ts
packages/contracts/src/learning/semantic-workflow-interpretation.ts
packages/contracts/src/learning/validation-run.ts
packages/contracts/src/learning/test-policy.ts
packages/contracts/src/learning/lifecycle-event.ts
packages/contracts/src/learning/receipts.ts
packages/contracts/src/learning/checkpoint-receipt.ts
packages/contracts/src/learning/observation-adapter.ts
packages/contracts/src/learning/observed-action.ts
packages/contracts/src/learning/boundary.ts
packages/contracts/src/learning/route-contracts.ts
packages/contracts/src/learning/pending-ability-ui-state.ts
packages/contracts/src/abilities/lookup.ts
packages/contracts/src/search/provider-kind.ts
packages/contracts/src/common/category.ts
```

#### EC service
```text
apps/ec-service/src/learning/create-learn-session.ts
apps/ec-service/src/learning/state-machine.ts
apps/ec-service/src/learning/update-runtime-snapshot.ts
apps/ec-service/src/learning/compute-semantic-interpretation.ts
apps/ec-service/src/learning/run-validation.ts
apps/ec-service/src/learning/ability-lookup.ts
apps/ec-service/src/learning/apply-test-policy.ts
apps/ec-service/src/learning/emit-learning-receipt.ts
apps/ec-service/src/learning/refresh-ability-availability.ts
apps/ec-service/src/learning/handle-checkpoint-verification.ts
apps/ec-service/src/learning/lint-checkpoints.ts
apps/ec-service/src/learning/install-saga.ts
apps/ec-service/src/learning/materialize-skill.ts
apps/ec-service/src/learning/trigger-bridge-rebuild.ts
apps/ec-service/src/learning/boundary-detector.ts
apps/ec-service/src/learning/observation-adapter-manager.ts
apps/ec-service/src/learning/proposal-revision-service.ts
apps/ec-service/src/learning/session-cleanup.ts
apps/ec-service/src/categories/category-service.ts
```

#### Q frontend
```text
apps/q-frontend/src/features/learn/AbilityLearningPage.tsx
apps/q-frontend/src/features/learn/LearnNewAbilityCards.tsx
apps/q-frontend/src/features/learn/ActiveCapabilityLearningView.tsx
apps/q-frontend/src/features/learn/PendingAbilitiesView.tsx
apps/q-frontend/src/features/learn/LearnHistoryView.tsx
apps/q-frontend/src/features/learn/LearnProposalDetail.tsx
apps/q-frontend/src/features/learn/LearnSessionDetail.tsx
apps/q-frontend/src/features/learn/LearnSessionOverlay.tsx
apps/q-frontend/src/features/learn/LearnSessionTimeline.tsx
apps/q-frontend/src/features/learn/CheckpointProgressView.tsx
apps/q-frontend/src/features/learn/ObservationModeControl.tsx
apps/q-frontend/src/features/abilities/AbilityAvailabilityPanel.tsx
apps/q-frontend/src/features/categories/CategoryAssignmentMenu.tsx
apps/q-frontend/src/features/categories/CategoryBrowser.tsx
```

---

### §0C.18 Deprecated labels and concepts

Treat the following inherited labels as deprecated UI aliases only:
- Teach Elnor
- Learning Center
- Experimental (as primary Learn tab)
- Experimental Abilities
- Observe My Actions (as a top-level learning mode)
- Private Trial Skill

Authoritative replacements:
- Ability Learning
- Pending Abilities
- Demonstrating Skill
- Coaching Agent
- Autonomous Agent Practice
- Improving Existing Skill
- Import Skill
- `Watch My Actions` is an alias only when it maps exactly to create-session observation mode; it must not create a second runtime control.
- `Request More Tests` may be used as a display alias for `POST /api/learn/proposals/:proposalId/request-more-tests`, but it must not create a second lifecycle route family.

### §0C.18A MCP, M365, Acrobat, and operational connector contracts (authoritative)

The four custom ELNOR-owned MCP wrappers remain **out of active build scope** in R11.2. Day-one active MCP / adapter scope in DOC3 is limited to the normalized management layer, active external/provider connectors, and the Acrobat adapter family defined below. Removed custom wrappers must not be treated as shipped connector surfaces, active module requirements, or mandatory Q-rendered cards.

#### §0.2D MCP management, auth, policy, ask-first, health, and composed-list contracts (authoritative)

```ts
// packages/contracts/src/mcp/shared.ts
import { z } from "zod";

export const MCPProviderSchema = z.enum([
  "m365",
  "gmail_google",
  "acrobat",
  "custom_remote",
  "provider_native",
]);

export const MCPTransportSchema = z.enum([
  "remote_http",
  "local_bridge",
  "provider_native",
]);

export const MCPAuthModeSchema = z.enum([
  "oauth2_delegated",
  "oauth2_on_behalf_of",
  "oauth2_client_credentials",
  "api_key",
  "none",
]);

export const MCPInteractionModeSchema = z.enum([
  "interactive_user",
  "background_service",
]);

export const MCPThrottleStateSchema = z.object({
  state: z.enum(["none", "cooldown", "server_backoff", "rate_limited"]).default("none"),
  retry_after_ms: z.number().int().min(0).optional(),
  reason_code: z.string().max(120).optional(),
  last_http_status: z.number().int().min(100).max(599).optional(),
  schema_version: z.literal(1),
});

export const MCPAuthProfileSchema = z.object({
  auth_profile_id: z.string().max(120),
  provider: MCPProviderSchema,
  auth_mode: MCPAuthModeSchema.default("oauth2_delegated"),
  secret_ref: z.string().max(240).optional(),
  token_ref: z.string().max(240).optional(),
  scopes: z.array(z.string().max(160)).default([]),
  tenant_id: z.string().max(120).optional(),
  client_id: z.string().max(120).optional(),
  redirect_uri: z.string().url().optional(),
  token_subject_kind: z.enum(["user", "service", "delegated"]).default("user"),
  incremental_consent_supported: z.boolean().default(false),
  auth_challenge_supported: z.boolean().default(false),
  expires_at: z.string().datetime().optional(),
  refresh_at: z.string().datetime().optional(),
  refresh_policy: z.enum(["manual", "auto_if_refresh_token", "deny"]).default("manual"),
  status: z.enum(["healthy", "challenge_required", "expired", "revoked", "unknown"]).default("unknown"),
  last_auth_error_code: z.string().max(120).optional(),
  shared_profile: z.boolean().default(false),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPServerRegistryEntrySchema = z.object({
  server_id: z.string().max(120),
  provider: MCPProviderSchema,
  display_name: z.string().max(160),
  endpoint_url: z.string().url().optional(),
  transport: MCPTransportSchema,
  interaction_mode: MCPInteractionModeSchema.default("interactive_user"),
  auth_profile_id: z.string().max(120).optional(),
  protocol_revision: z.string().max(40).optional(),
  tool_schema_hash: z.string().max(128).optional(),
  rate_limit_profile: z.enum(["unknown", "low", "medium", "high"]).default("unknown"),
  data_classes: z.array(z.string().max(80)).default([]),
  supported_tools: z.array(z.string().max(120)).default([]),
  enabled: z.boolean().default(true),
  active_build_surface: z.boolean().default(true),
  supports_smoke_test: z.boolean().default(true),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPToolHealthSchema = z.object({
  ok: z.boolean(),
  last_error: z.string().max(240).optional(),
  last_checked_at: z.string().datetime().optional(),
  last_http_status: z.number().int().min(100).max(599).optional(),
});

export const MCPConnectionHealthSchema = z.object({
  server_id: z.string().max(120),
  health_status: z.enum(["healthy", "degraded", "disabled", "unknown"]),
  health_reason_code: z.string().max(120).optional(),
  last_checked_at: z.string().datetime().optional(),
  failure_count_rolling: z.number().int().min(0).default(0),
  backoff_until: z.string().datetime().optional(),
  throttle_state: MCPThrottleStateSchema.default({
    state: "none",
    schema_version: 1,
  }),
  auth_state: z.enum(["healthy", "challenge_required", "expired", "revoked", "unknown"]).default("unknown"),
  interactive_capable: z.boolean().default(true),
  tool_health: z.record(z.string().max(120), MCPToolHealthSchema).default({}),
  schema_version: z.literal(1),
});

export const MCPServerAuthStatusSchema = z.object({
  server_id: z.string().max(120),
  auth_profile_id: z.string().max(120).optional(),
  auth_state: z.enum(["healthy", "challenge_required", "expired", "revoked", "unknown"]),
  auth_mode: MCPAuthModeSchema.optional(),
  scopes_granted: z.array(z.string().max(160)).default([]),
  scopes_missing: z.array(z.string().max(160)).default([]),
  expires_at: z.string().datetime().optional(),
  refresh_at: z.string().datetime().optional(),
  redirect_uri: z.string().url().optional(),
  last_auth_error_code: z.string().max(120).optional(),
  schema_version: z.literal(1),
});

export const MCPPolicyTargetSchema = z.discriminatedUnion("target_kind", [
  z.object({
    target_kind: z.literal("global"),
  }),
  z.object({
    target_kind: z.literal("provider"),
    provider: MCPProviderSchema,
  }),
  z.object({
    target_kind: z.literal("server"),
    server_id: z.string().max(120),
  }),
  z.object({
    target_kind: z.literal("tool"),
    server_id: z.string().max(120),
    tool_name: z.string().max(120),
  }),
  z.object({
    target_kind: z.literal("temporary_grant"),
    dispatch_id: z.string().uuid(),
  }),
]);

export const MCPPolicyDecisionSchema = z.object({
  mode: z.enum(["enabled", "disabled", "ask_first"]),
  expires_at: z.string().datetime().optional(),
  reason_code: z.string().max(120).optional(),
  updated_by: z.enum(["user", "system", "migration"]),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPEffectivePolicySchema = z.object({
  target: MCPPolicyTargetSchema,
  decision: MCPPolicyDecisionSchema,
  inherited_from: z.enum(["none", "global", "provider", "server"]).default("none"),
  schema_version: z.literal(1),
});

export const MCPServerCreateRequestSchema = z.object({
  provider: MCPProviderSchema,
  display_name: z.string().max(160),
  endpoint_url: z.string().url().optional(),
  transport: MCPTransportSchema,
  interaction_mode: MCPInteractionModeSchema.default("interactive_user"),
  auth_profile_id: z.string().max(120).optional(),
  data_classes: z.array(z.string().max(80)).default([]),
  supported_tools: z.array(z.string().max(120)).default([]),
  enabled: z.boolean().default(true),
  schema_version: z.literal(1),
}).strict();

export const MCPServerPatchRequestSchema = z.object({
  display_name: z.string().max(160).optional(),
  endpoint_url: z.string().url().optional(),
  auth_profile_id: z.string().max(120).nullable().optional(),
  enabled: z.boolean().optional(),
  data_classes: z.array(z.string().max(80)).optional(),
  supported_tools: z.array(z.string().max(120)).optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPServerDeleteResponseSchema = z.object({
  server_id: z.string().max(120),
  deleted: z.boolean(),
  auth_profile_action: z.enum([
    "none",
    "retained_shared",
    "retained_unreferenced",
    "deleted_unreferenced",
  ]),
  detached_auth_profile_id: z.string().max(120).optional(),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthProfileCreateRequestSchema = z.object({
  provider: MCPProviderSchema,
  auth_mode: MCPAuthModeSchema,
  scopes: z.array(z.string().max(160)).default([]),
  tenant_id: z.string().max(120).optional(),
  client_id: z.string().max(120).optional(),
  redirect_uri: z.string().url().optional(),
  token_subject_kind: z.enum(["user", "service", "delegated"]).default("user"),
  refresh_policy: z.enum(["manual", "auto_if_refresh_token", "deny"]).default("manual"),
  shared_profile: z.boolean().default(false),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthProfilePatchRequestSchema = z.object({
  scopes: z.array(z.string().max(160)).optional(),
  redirect_uri: z.string().url().nullable().optional(),
  refresh_policy: z.enum(["manual", "auto_if_refresh_token", "deny"]).optional(),
  shared_profile: z.boolean().optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthProfileDeleteResponseSchema = z.object({
  auth_profile_id: z.string().max(120),
  deleted: z.boolean(),
  blocked_by_server_ids: z.array(z.string().max(120)).default([]),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthStartRequestSchema = z.object({
  server_id: z.string().max(120),
  requested_scopes_override: z.array(z.string().max(160)).optional(),
  interactive_hint: z.enum(["same_window", "popup", "new_tab"]).default("same_window"),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthStartResponseSchema = z.object({
  server_id: z.string().max(120),
  auth_profile_id: z.string().max(120),
  authorization_url: z.string().url(),
  oauth_state_ref: z.string().max(240),
  pkce_verifier_ref: z.string().max(240),
  expires_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPAuthCallbackResponseSchema = z.object({
  server_id: z.string().max(120),
  auth_profile_id: z.string().max(120),
  auth_state: z.enum(["healthy", "challenge_required", "expired", "revoked", "unknown"]),
  granted_scopes: z.array(z.string().max(160)).default([]),
  missing_scopes: z.array(z.string().max(160)).default([]),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthRefreshRequestSchema = z.object({
  server_id: z.string().max(120),
  force: z.boolean().default(false),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthRevokeRequestSchema = z.object({
  server_id: z.string().max(120),
  revoke_remote: z.boolean().default(false),
  keep_auth_profile: z.boolean().default(true),
  schema_version: z.literal(1),
}).strict();

export const MCPAskFirstDispatchRecordSchema = z.object({
  dispatch_id: z.string().uuid(),
  correlation_id: z.string().uuid(),
  server_id: z.string().max(120),
  tool_name: z.string().max(120),
  request_summary: z.string().max(500),
  request_payload_ref: z.string().max(240).optional(),
  status: z.enum([
    "pending",
    "approved_once",
    "approved_until_end_of_turn",
    "approved_for_session",
    "denied",
    "expired",
    "cancelled",
    "resumed",
  ]),
  paused_dispatch_ref: z.string().max(240),
  effective_policy_target: MCPPolicyTargetSchema,
  expires_at: z.string().datetime(),
  decision_reason: z.string().max(500).optional(),
  approved_by: z.string().max(160).optional(),
  decided_at: z.string().datetime().optional(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPAskFirstDecisionRequestSchema = z.object({
  decision: z.enum([
    "approve_once",
    "approve_until_end_of_turn",
    "approve_for_session",
    "deny",
  ]),
  reason: z.string().max(500).optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPAskFirstDecisionResponseSchema = z.object({
  dispatch: MCPAskFirstDispatchRecordSchema,
  resumed: z.boolean(),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPBadgeStateSchema = z.enum([
  "connected",
  "approval_required",
  "auth_required",
  "degraded",
  "disabled",
  "deferred",
  "background_only",
]);

export const MCPServerListItemSchema = z.object({
  registry: MCPServerRegistryEntrySchema,
  health: MCPConnectionHealthSchema,
  auth: MCPServerAuthStatusSchema.optional(),
  effective_policy: MCPEffectivePolicySchema,
  badge_states: z.array(MCPBadgeStateSchema).default([]),
  available_actions: z.array(z.enum([
    "connect_auth",
    "refresh_auth",
    "revoke_auth",
    "edit",
    "delete",
    "smoke_test",
    "policy_update",
  ])).default([]),
  last_receipt_id: z.string().uuid().optional(),
  deferred_design_only: z.boolean().default(false),
  schema_version: z.literal(1),
});

export const MCPServerListResponseSchema = z.object({
  servers: z.array(MCPServerListItemSchema),
  generated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const MCPReceiptSummarySchema = z.object({
  receipt_id: z.string().uuid(),
  server_id: z.string().max(120).optional(),
  receipt_kind: z.enum([
    "auth_start",
    "auth_callback",
    "auth_refresh",
    "auth_revoke",
    "smoke_test",
    "policy_update",
    "ask_first_decision",
    "migration_backfill",
    "server_create",
    "server_patch",
    "server_delete",
    "auth_profile_delete",
  ]),
  status: z.enum(["ok", "degraded", "blocked", "failed"]),
  created_at: z.string().datetime(),
  summary: z.string().max(500),
  correlation_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPReceiptListResponseSchema = z.object({
  receipts: z.array(MCPReceiptSummarySchema).default([]),
  next_cursor: z.string().max(240).optional(),
  schema_version: z.literal(1),
});
```

##### MCP route / response normalization additions (authoritative)

```ts
// packages/contracts/src/mcp/ops-normalized.ts
import { z } from "zod";
import {
  MCPServerListItemSchema,
  MCPServerAuthStatusSchema,
  MCPAskFirstDispatchRecordSchema,
} from "./shared";

export const MCPLinkedAbilityRefSchema = z.object({
  ability_id: z.string().max(160),
  label: z.string().max(160).optional(),
  relation: z.enum(["primary", "secondary", "suggested"]).default("primary"),
});

export const MCPServerMutationResponseSchema = z.object({
  server: MCPServerListItemSchema.extend({
    linked_abilities: z.array(MCPLinkedAbilityRefSchema).default([]),
  }),
  updated: z.boolean().default(true),
  accepted: z.boolean().default(true),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthStartResponseSchema = z.object({
  server_id: z.string().max(120),
  auth_url: z.string().url(),
  challenge_id: z.string().max(160).optional(),
  correlation_id: z.string().uuid().optional(),
  expires_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthCallbackQuerySchema = z.object({
  code: z.string().max(4000).optional(),
  state: z.string().max(512),
  error: z.string().max(160).optional(),
  error_description: z.string().max(2000).optional(),
  error_uri: z.string().url().optional(),
  session_state: z.string().max(512).optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthRevokeResponseSchema = z.object({
  server_id: z.string().max(120),
  revoked: z.boolean(),
  status: z.enum(["revoked", "already_revoked", "noop"]),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPSmokeTestRequestSchema = z.object({
  server_id: z.string().max(120),
  tool_name: z.string().max(160).optional(),
  payload_preview: z.record(z.string(), z.unknown()).optional(),
  correlation_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPSmokeTestResponseSchema = z.object({
  server_id: z.string().max(120),
  smoke_test_id: z.string().uuid(),
  status: z.enum(["passed", "failed", "blocked", "degraded"]),
  receipt_id: z.string().uuid().optional(),
  started_at: z.string().datetime(),
  completed_at: z.string().datetime().optional(),
  error_code: z.string().max(160).optional(),
  summary: z.string().max(500).optional(),
  schema_version: z.literal(1),
});

export const MCPAuthChallengeRespondRequestSchema = z.object({
  challenge_id: z.string().max(160),
  decision: z.enum(["approve", "deny", "retry"]),
  note: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const MCPAuthChallengeRespondResponseSchema = z.object({
  challenge_id: z.string().max(160),
  server_id: z.string().max(120),
  resolved: z.boolean(),
  resolution: z.enum(["approved", "denied", "retried", "expired"]),
  auth_status: MCPServerAuthStatusSchema,
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const MCPAuthProfileListResponseSchema = z.object({
  auth_profiles: z.array(MCPAuthProfileSchema).default([]),
  schema_version: z.literal(1),
});

export const MCPAskFirstPendingListResponseSchema = z.object({
  dispatches: z.array(MCPAskFirstDispatchRecordSchema).default([]),
  schema_version: z.literal(1),
});
```

Within the canonical `MCPServerListItemSchema`, add the following exact field immediately after `badge_states`:

```ts
linked_abilities: z.array(MCPLinkedAbilityRefSchema).default([]),
```

##### Public EC management / read-model routes (authoritative)

```http
GET    /api/mcp/servers
POST   /api/mcp/servers/register
PATCH  /api/mcp/servers/:serverId
DELETE /api/mcp/servers/:serverId
GET    /api/mcp/servers/:serverId/health
GET    /api/mcp/servers/:serverId/auth/status
POST   /api/mcp/servers/:serverId/auth/start
GET    /api/mcp/servers/:serverId/auth/callback
POST   /api/mcp/servers/:serverId/auth/refresh
POST   /api/mcp/servers/:serverId/auth/revoke
POST   /api/mcp/servers/:serverId/smoke-test
GET    /api/mcp/auth-profiles
POST   /api/mcp/auth-profiles
PATCH  /api/mcp/auth-profiles/:authProfileId
DELETE /api/mcp/auth-profiles/:authProfileId
POST   /api/mcp/policy/update
GET    /api/mcp/receipts
GET    /api/mcp/ask-first/pending
POST   /api/mcp/ask-first/:dispatchId/approve
POST   /api/mcp/ask-first/:dispatchId/deny
POST   /api/mcp/auth/challenge/respond
```

##### Route request / response map

| Route | Request schema | Success response |
|---|---|---|
| `GET /api/mcp/servers` | none | `MCPServerListResponseSchema` |
| `POST /api/mcp/servers/register` | `MCPServerCreateRequestSchema` | `MCPServerMutationResponseSchema` |
| `PATCH /api/mcp/servers/:serverId` | `MCPServerPatchRequestSchema` | `MCPServerMutationResponseSchema` |
| `DELETE /api/mcp/servers/:serverId` | none | `MCPServerDeleteResponseSchema` |
| `GET /api/mcp/servers/:serverId/health` | none | `MCPConnectionHealthSchema` |
| `GET /api/mcp/servers/:serverId/auth/status` | none | `MCPServerAuthStatusSchema` |
| `POST /api/mcp/servers/:serverId/auth/start` | `MCPAuthStartRequestSchema` | `MCPAuthStartResponseSchema` |
| `GET /api/mcp/servers/:serverId/auth/callback` | `MCPAuthCallbackQuerySchema` | `MCPServerAuthStatusSchema` |
| `POST /api/mcp/servers/:serverId/auth/refresh` | `MCPAuthRefreshRequestSchema` | `MCPServerAuthStatusSchema` |
| `POST /api/mcp/servers/:serverId/auth/revoke` | `MCPAuthRevokeRequestSchema` | `MCPAuthRevokeResponseSchema` |
| `POST /api/mcp/servers/:serverId/smoke-test` | `MCPSmokeTestRequestSchema` | `MCPSmokeTestResponseSchema` |
| `GET /api/mcp/auth-profiles` | none | `MCPAuthProfileListResponseSchema` |
| `POST /api/mcp/auth-profiles` | `MCPAuthProfileCreateRequestSchema` | `MCPAuthProfileSchema` |
| `PATCH /api/mcp/auth-profiles/:authProfileId` | `MCPAuthProfilePatchRequestSchema` | `MCPAuthProfileSchema` |
| `DELETE /api/mcp/auth-profiles/:authProfileId` | none | `MCPAuthProfileDeleteResponseSchema` |
| `POST /api/mcp/policy/update` | `{ target: MCPPolicyTargetSchema; decision: MCPPolicyDecisionSchema; schema_version: 1 }` | `MCPEffectivePolicySchema` |
| `GET /api/mcp/receipts` | none | `MCPReceiptListResponseSchema` |
| `GET /api/mcp/ask-first/pending` | none | `MCPAskFirstPendingListResponseSchema` |
| `POST /api/mcp/ask-first/:dispatchId/approve` | `MCPAskFirstDecisionRequestSchema` with `decision != "deny"` | `MCPAskFirstDecisionResponseSchema` |
| `POST /api/mcp/ask-first/:dispatchId/deny` | `MCPAskFirstDecisionRequestSchema` with `decision = "deny"` | `MCPAskFirstDecisionResponseSchema` |
| `POST /api/mcp/auth/challenge/respond` | `MCPAuthChallengeRespondRequestSchema` | `MCPAuthChallengeRespondResponseSchema` |

##### Public EC route vs shared internal contract boundary

DOC3 distinguishes between:

**A. public EC management / read-model routes**  
The routes listed above are user/Q-callable and must exist as HTTP routes.

**B. shared internal operation contracts**  
Typed connector-operation contracts used by DOC4 / DOC11 / OpenClaw bridge/runtime dispatch remain shared internal contracts. Their existence does **not** authorize Q to call one public REST route per tool family.

**C. poll-first v1 health rule**  
Until DOC11 defines a push contract explicitly, Q must derive connector card truth by polling:
- `GET /api/mcp/servers`
- `GET /api/mcp/servers/:serverId/health`
- `GET /api/mcp/servers/:serverId/auth/status`
- `GET /api/mcp/ask-first/pending`

No silent assumed WebSocket/SSE connector-health channel is part of first-wave DOC3 unless DOC11 later adds it.

##### Shared-auth-profile delete safety rule

1. `DELETE /api/mcp/servers/:serverId` must never delete a shared auth profile merely because the server is deleted.
2. If the deleted server references an auth profile used by any other server, the response must set:
   - `auth_profile_action = "retained_shared"`
3. If an auth profile is no longer referenced after server deletion, EC may:
   - retain it and return `retained_unreferenced`, or
   - delete it and return `deleted_unreferenced`
   only when the profile is explicitly marked disposable by EC policy.
4. `DELETE /api/mcp/auth-profiles/:authProfileId` must fail with `MCP_AUTH_PROFILE_IN_USE` if any active server still references the profile.
5. No `force=true` bypass is permitted in DOC3.

##### OAuth / PKCE lifecycle (authoritative)

1. `POST /api/mcp/servers/:serverId/auth/start` must:
   - validate server + auth profile + route policy,
   - generate a durable `oauth_state_ref`,
   - generate a durable `pkce_verifier_ref` when PKCE is used,
   - emit an `auth_start` receipt,
   - and return `MCPAuthStartResponseSchema`.
2. `GET /api/mcp/servers/:serverId/auth/callback` must:
   - validate the returned state using `MCPAuthCallbackQuerySchema`,
   - reject reused or expired state with `MCP_OAUTH_STATE_INVALID`,
   - exchange the code for token refs,
   - update `MCPAuthProfileSchema.status`,
   - compute `scopes_granted` and `scopes_missing`,
   - emit an `auth_callback` receipt,
   - and return `MCPServerAuthStatusSchema`.
3. `POST /api/mcp/servers/:serverId/auth/refresh` must:
   - succeed only when refresh is allowed by `refresh_policy`,
   - reject with `MCP_OAUTH_REFRESH_NOT_ALLOWED` otherwise,
   - update `expires_at`, `refresh_at`, and `status`,
   - and return `MCPServerAuthStatusSchema`.
4. Runtime dispatch may auto-refresh only when:
   - `refresh_policy = "auto_if_refresh_token"`
   - a refresh token ref exists
   - and no interactive auth challenge is required.
5. If runtime dispatch encounters auth expiry and cannot auto-refresh, EC must:
   - set `auth_state = "challenge_required"` or `expired`,
   - write a receipt,
   - and surface an actionable badge in `MCPServerListResponseSchema`.

##### Ask-first lifecycle (authoritative)

1. Ask-first is **not** the same as auth challenge.
2. When policy requires approval, EC must:
   - pause the dispatch,
   - create an `MCPAskFirstDispatchRecordSchema` record,
   - assign a `correlation_id` shared by the paused dispatch and the approval record,
   - emit an `ask_first` event/receipt,
   - and expose the pending approval through `GET /api/mcp/ask-first/pending`.
3. On approve:
   - EC records the chosen approval scope,
   - optionally writes a temporary-grant policy target when appropriate,
   - resumes the paused dispatch,
   - returns `resumed = true`.
4. On deny:
   - the paused dispatch resolves to blocked,
   - a receipt is emitted,
   - no hidden retry occurs.
5. Expired ask-first records must transition to `expired`; Q must render them as expired, not silently remove them.
6. `correlation_id` must be exposed in both receipts and the approval record so UI and logs can reconcile the action.

##### Composed connector-list / badge-truth rule

`GET /api/mcp/servers` is the single authoritative source for connector cards and badges in Q. Q must derive connector badge state from `MCPServerListItemSchema.badge_states`, not from frontend heuristics.

At minimum:
- `auth_required` requires `auth_state in {"challenge_required","expired","revoked"}`
- `approval_required` requires policy mode `ask_first`
- `degraded` requires `health_status = "degraded"`
- `disabled` requires `enabled = false`
- `deferred` requires `active_build_surface = false` or `deferred_design_only = true`
- `background_only` requires `interaction_mode = "background_service"`
- the UI label `Linked skills` must bind to `linked_abilities`, not to an ad hoc second field.

##### Route-scoring pseudocode rule

Any route planner or connector scorer added by DOC10 / DOC11 must use the composed server/health/auth views, not a stale registry-only object. The minimum pseudocode is:

```ts
const serverView = serverList.servers.find((item) => item.server_id === candidate.server_id);
if (!serverView || !serverView.enabled) return "reject_disabled";
if (serverView.health_status === "degraded") return "defer_or_warn";
if (serverView.auth_state in { challenge_required: true, expired: true, revoked: true }) return "auth_required";
if (serverView.badge_states.includes("approval_required")) return "ask_first";
return "eligible";
```

#### §0.2E Microsoft 365 connector contract family (authoritative)

```ts
// packages/contracts/src/mcp/m365.ts
import { z } from "zod";
import {
  MCPAuthProfileSchema,
  MCPInteractionModeSchema,
  MCPThrottleStateSchema,
} from "./shared";

export const M365OperationFamilySchema = z.enum([
  "search",
  "sharepoint_list_documents",
  "onedrive_fetch_file",
  "project_get_latest_document",
  "outlook_mail_search",
  "outlook_get_thread",
  "outlook_draft_reply",
  "calendar_check_availability",
  "calendar_create_event",
  "calendar_update_event",
  "word_fetch_content",
  "word_fetch_comments",
  "teams_post_message",
  "teams_get_channel_context",
]);

export const M365ScopeRuleSchema = z.object({
  operation_family: M365OperationFamilySchema,
  minimum_delegated_scopes: z.array(z.string().max(160)).min(1),
  optional_delegated_scopes: z.array(z.string().max(160)).default([]),
  admin_consent_likely: z.boolean().default(false),
  delegated_only: z.boolean().default(false),
  schema_version: z.literal(1),
});

export const M365ConnectorRegistrationSchema = z.object({
  server_id: z.string().max(120),
  provider: z.literal("m365"),
  display_name: z.string().max(160),
  operation_families: z.array(M365OperationFamilySchema).min(1),
  interaction_mode: MCPInteractionModeSchema.default("interactive_user"),
  auth_profile_id: z.string().max(120),
  requested_scopes: z.array(z.string().max(160)).min(1),
  tenant_id: z.string().max(120).optional(),
  client_id: z.string().max(120),
  redirect_uri: z.string().url().optional(),
  incremental_consent_supported: z.boolean().default(true),
  verified_obo_chain: z.boolean().default(false),
  allow_background_service_ops: z.boolean().default(false),
  schema_version: z.literal(1),
});

export const M365OperationContextSchema = z.object({
  server_id: z.string().max(120),
  operation_family: M365OperationFamilySchema,
  interaction_mode: MCPInteractionModeSchema.default("interactive_user"),
  auth_profile_id: z.string().max(120).optional(),
  dispatch_id: z.string().max(200).optional(),
  project_id: z.string().max(160).optional(),
  room_id: z.string().max(160).optional(),
  timeout_ms: z.number().int().min(1000).max(120000).default(30000),
  schema_version: z.literal(1),
});

const OutlookTargetByThreadSchema = z.object({
  thread_id: z.string().max(240),
  message_id: z.undefined().optional(),
});

const OutlookTargetByMessageSchema = z.object({
  thread_id: z.undefined().optional(),
  message_id: z.string().max(240),
});

const OutlookTargetSchema = z.union([
  OutlookTargetByThreadSchema,
  OutlookTargetByMessageSchema,
]);

export const M365SearchRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("search"),
  query: z.string().min(1).max(1000),
  entity_filters: z.array(z.enum(["files", "sites", "mail", "people"])).default([]),
  max_results: z.number().int().min(1).max(100).default(20),
});

export const M365SharePointListDocumentsRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("sharepoint_list_documents"),
  site_ref: z.string().max(240),
  drive_ref: z.string().max(240).optional(),
  folder_ref: z.string().max(240).optional(),
  max_results: z.number().int().min(1).max(200).default(50),
});

export const M365OneDriveFetchFileRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("onedrive_fetch_file"),
  drive_item_ref: z.string().max(240),
  include_content: z.boolean().default(false),
});

export const M365ProjectLatestDocumentRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("project_get_latest_document"),
  project_id: z.string().max(160),
  document_role: z.string().max(120).optional(),
  title_hint: z.string().max(240).optional(),
});

export const M365OutlookMailSearchRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("outlook_mail_search"),
  query: z.string().min(1).max(1000),
  folder: z.string().max(120).optional(),
  max_results: z.number().int().min(1).max(100).default(25),
});

export const M365OutlookGetThreadRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("outlook_get_thread"),
}).and(OutlookTargetSchema);

export const M365OutlookDraftReplyRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("outlook_draft_reply"),
  body_markdown: z.string().max(40000),
  subject_override: z.string().max(240).optional(),
}).and(OutlookTargetSchema);

export const M365CalendarCheckAvailabilityRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("calendar_check_availability"),
  attendee_emails: z.array(z.string().email()).default([]),
  time_min: z.string().datetime(),
  time_max: z.string().datetime(),
  duration_minutes: z.number().int().min(15).max(480).optional(),
});

export const M365CalendarCreateEventRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("calendar_create_event"),
  title: z.string().max(240),
  start_time: z.string().datetime(),
  end_time: z.string().datetime(),
  attendee_emails: z.array(z.string().email()).default([]),
  location: z.string().max(240).optional(),
  body_markdown: z.string().max(20000).optional(),
});

export const M365CalendarUpdateEventRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("calendar_update_event"),
  event_id: z.string().max(240),
  title: z.string().max(240).optional(),
  start_time: z.string().datetime().optional(),
  end_time: z.string().datetime().optional(),
  attendee_emails: z.array(z.string().email()).optional(),
  location: z.string().max(240).optional(),
  body_markdown: z.string().max(20000).optional(),
});

export const M365WordFetchContentRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("word_fetch_content"),
  document_ref: z.string().max(240),
  include_comments: z.boolean().default(false),
});

export const M365WordFetchCommentsRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("word_fetch_comments"),
  document_ref: z.string().max(240),
  max_results: z.number().int().min(1).max(500).default(100),
});

export const M365TeamsPostMessageRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("teams_post_message"),
  target_kind: z.enum(["channel", "chat"]),
  target_ref: z.string().max(240),
  message_markdown: z.string().max(20000),
});

export const M365TeamsGetChannelContextRequestSchema = M365OperationContextSchema.extend({
  operation_family: z.literal("teams_get_channel_context"),
  team_id: z.string().max(240),
  channel_id: z.string().max(240),
  include_recent_messages: z.boolean().default(false),
});

export const M365OperationRequestSchema = z.discriminatedUnion("operation_family", [
  M365SearchRequestSchema,
  M365SharePointListDocumentsRequestSchema,
  M365OneDriveFetchFileRequestSchema,
  M365ProjectLatestDocumentRequestSchema,
  M365OutlookMailSearchRequestSchema,
  M365OutlookGetThreadRequestSchema,
  M365OutlookDraftReplyRequestSchema,
  M365CalendarCheckAvailabilityRequestSchema,
  M365CalendarCreateEventRequestSchema,
  M365CalendarUpdateEventRequestSchema,
  M365WordFetchContentRequestSchema,
  M365WordFetchCommentsRequestSchema,
  M365TeamsPostMessageRequestSchema,
  M365TeamsGetChannelContextRequestSchema,
]);

export const M365SearchResultSchema = z.object({
  operation_family: z.literal("search"),
  hits: z.array(z.object({
    hit_id: z.string().max(240),
    source_kind: z.enum(["file", "site", "mail", "person"]),
    title: z.string().max(240),
    url: z.string().url().optional(),
    modified_at: z.string().datetime().optional(),
  })).default([]),
});

export const M365DocumentListResultSchema = z.object({
  operation_family: z.literal("sharepoint_list_documents"),
  documents: z.array(z.object({
    document_ref: z.string().max(240),
    title: z.string().max(240),
    modified_at: z.string().datetime().optional(),
    url: z.string().url().optional(),
  })).default([]),
});

export const M365FetchFileResultSchema = z.object({
  operation_family: z.literal("onedrive_fetch_file"),
  document_ref: z.string().max(240),
  file_name: z.string().max(240),
  download_url: z.string().url().optional(),
  content_ref: z.string().max(240).optional(),
});

export const M365ProjectLatestDocumentResultSchema = z.object({
  operation_family: z.literal("project_get_latest_document"),
  project_id: z.string().max(160),
  document_ref: z.string().max(240).optional(),
  resolved_via: z.enum(["project_binding", "connector_search", "none"]),
});

export const M365MailSearchResultSchema = z.object({
  operation_family: z.literal("outlook_mail_search"),
  messages: z.array(z.object({
    message_id: z.string().max(240),
    subject: z.string().max(240),
    from: z.string().max(240).optional(),
    received_at: z.string().datetime().optional(),
  })).default([]),
});

export const M365ThreadResultSchema = z.object({
  operation_family: z.literal("outlook_get_thread"),
  thread_id: z.string().max(240),
  message_ids: z.array(z.string().max(240)).default([]),
});

export const M365DraftReplyResultSchema = z.object({
  operation_family: z.literal("outlook_draft_reply"),
  draft_id: z.string().max(240),
  thread_id: z.string().max(240).optional(),
  status: z.enum(["draft_created", "draft_updated"]),
});

export const M365AvailabilityResultSchema = z.object({
  operation_family: z.literal("calendar_check_availability"),
  slots: z.array(z.object({
    start_time: z.string().datetime(),
    end_time: z.string().datetime(),
    state: z.enum(["free", "busy", "tentative", "unknown"]),
  })).default([]),
});

export const M365CalendarCreateEventResultSchema = z.object({
  operation_family: z.literal("calendar_create_event"),
  event_id: z.string().max(240),
  status: z.literal("created"),
});

export const M365CalendarUpdateEventResultSchema = z.object({
  operation_family: z.literal("calendar_update_event"),
  event_id: z.string().max(240),
  status: z.literal("updated"),
});

export const M365WordContentResultSchema = z.object({
  operation_family: z.literal("word_fetch_content"),
  document_ref: z.string().max(240),
  content_ref: z.string().max(240).optional(),
  comment_count: z.number().int().min(0).default(0),
});

export const M365WordCommentsResultSchema = z.object({
  operation_family: z.literal("word_fetch_comments"),
  document_ref: z.string().max(240),
  comments: z.array(z.object({
    comment_id: z.string().max(240),
    author: z.string().max(240).optional(),
    created_at: z.string().datetime().optional(),
    preview: z.string().max(500).optional(),
  })).default([]),
});

export const M365TeamsPostMessageResultSchema = z.object({
  operation_family: z.literal("teams_post_message"),
  message_id: z.string().max(240),
  target_kind: z.enum(["channel", "chat"]),
  target_ref: z.string().max(240),
});

export const M365TeamsChannelContextResultSchema = z.object({
  operation_family: z.literal("teams_get_channel_context"),
  team_id: z.string().max(240),
  channel_id: z.string().max(240),
  channel_name: z.string().max(240).optional(),
  recent_message_count: z.number().int().min(0).default(0),
});

export const M365OperationResultSchema = z.discriminatedUnion("operation_family", [
  M365SearchResultSchema,
  M365DocumentListResultSchema,
  M365FetchFileResultSchema,
  M365ProjectLatestDocumentResultSchema,
  M365MailSearchResultSchema,
  M365ThreadResultSchema,
  M365DraftReplyResultSchema,
  M365AvailabilityResultSchema,
  M365CalendarCreateEventResultSchema,
  M365CalendarUpdateEventResultSchema,
  M365WordContentResultSchema,
  M365WordCommentsResultSchema,
  M365TeamsPostMessageResultSchema,
  M365TeamsChannelContextResultSchema,
]);

export const M365OperationSuccessResponseSchema = z.object({
  operation_id: z.string().uuid(),
  server_id: z.string().max(120),
  operation_family: M365OperationFamilySchema,
  status: z.enum(["ok", "degraded"]),
  auth_profile_id: z.string().max(120).optional(),
  granted_scopes: z.array(z.string().max(160)).default([]),
  missing_scopes: z.array(z.string().max(160)).default([]),
  receipt_id: z.string().uuid().optional(),
  identity_asserted_kind: z.enum(["local_user", "oauth_user", "service_principal", "unknown"]).default("unknown"),
  identity_asserted_ref: z.string().max(240).optional(),
  throttle_state: MCPThrottleStateSchema.default({
    state: "none",
    schema_version: 1,
  }),
  result: M365OperationResultSchema,
  schema_version: z.literal(1),
});

export const M365OperationErrorResponseSchema = z.object({
  operation_id: z.string().uuid(),
  server_id: z.string().max(120),
  operation_family: M365OperationFamilySchema,
  status: z.enum(["blocked", "failed"]),
  auth_profile_id: z.string().max(120).optional(),
  granted_scopes: z.array(z.string().max(160)).default([]),
  missing_scopes: z.array(z.string().max(160)).default([]),
  receipt_id: z.string().uuid().optional(),
  identity_asserted_kind: z.enum(["local_user", "oauth_user", "service_principal", "unknown"]).default("unknown"),
  identity_asserted_ref: z.string().max(240).optional(),
  throttle_state: MCPThrottleStateSchema.default({
    state: "none",
    schema_version: 1,
  }),
  error_code: z.string().max(120),
  error_message: z.string().max(500).optional(),
  schema_version: z.literal(1),
});

export const M365OperationResponseSchema = z.union([
  M365OperationSuccessResponseSchema,
  M365OperationErrorResponseSchema,
]);
```

##### Minimum delegated scope matrix for first-wave M365 operation families

| Operation family | Minimum delegated scopes | Optional delegated scopes | Admin consent likely | Delegated only |
|---|---|---|---|---|
| `search` | `User.Read`, `Files.Read.All`, `Sites.Read.All` | `Mail.Read` when mail results are enabled | Yes | No |
| `sharepoint_list_documents` | `Sites.Read.All`, `Files.Read.All` | none | Yes | No |
| `onedrive_fetch_file` | `Files.Read.All` | `Files.Read` when only the signed-in user's drive is supported | No | No |
| `project_get_latest_document` | `Sites.Read.All`, `Files.Read.All` | `User.Read` | Yes | No |
| `outlook_mail_search` | `Mail.Read` | none | No | No |
| `outlook_get_thread` | `Mail.Read` | none | No | No |
| `outlook_draft_reply` | `Mail.ReadWrite` | `Mail.Send` when final-send is enabled in a later doc | No | No |
| `calendar_check_availability` | `Calendars.Read` | none | No | No |
| `calendar_create_event` | `Calendars.ReadWrite` | `OnlineMeetings.ReadWrite` when online-meeting creation is enabled | No | No |
| `calendar_update_event` | `Calendars.ReadWrite` | `OnlineMeetings.ReadWrite` when online-meeting update is enabled | No | No |
| `word_fetch_content` | `Files.Read.All`, `Sites.Read.All` | none | Yes | No |
| `word_fetch_comments` | `Files.Read.All`, `Sites.Read.All` | none | Yes | No |
| `teams_post_message` | channel target: `ChannelMessage.Send`; chat target: `Chat.ReadWrite` | `Team.ReadBasic.All`, `Channel.ReadBasic.All`, `ChatMessage.Send` when the concrete chat send path requires it | channel: No / chat: possibly tenant-dependent | **Yes** |
| `teams_get_channel_context` | `Team.ReadBasic.All`, `Channel.ReadBasic.All` | `ChannelMessage.Read.All` when recent channel messages are included | Often | No |

##### M365 auth-mode and registration rules

1. At the shared-schema level, `MCPAuthProfileSchema.auth_mode` may remain generic.
2. For `provider = "m365"` and `interaction_mode = "interactive_user"`:
   - omitted `auth_mode` normalizes to `oauth2_delegated`;
   - `oauth2_on_behalf_of` is allowed only when `verified_obo_chain = true`;
   - `oauth2_client_credentials`, `api_key`, and `none` must be rejected with `M365_FORBIDDEN_AUTH_MODE`.
3. For `provider = "m365"` and `interaction_mode = "background_service"`:
   - `oauth2_client_credentials` is allowed only when `allow_background_service_ops = true`;
   - `teams_post_message` is forbidden in `background_service` mode and must fail with `M365_DELEGATED_ONLY_OPERATION`.
4. `redirect_uri` is required for interactive M365 connectors.
5. `requested_scopes` must cover the minimum delegated union implied by all declared operation families, unless DOC4 documents a narrower tenant-approved equivalent.
6. No raw tokens or client secrets may be durably written by DOC3.

##### Required validator pseudocode

```ts
export function validateM365ConnectorRegistration(
  registration: z.infer<typeof M365ConnectorRegistrationSchema>,
  authProfile: z.infer<typeof MCPAuthProfileSchema>,
  minimumScopeMatrix: z.infer<typeof M365ScopeRuleSchema>[],
): string[] {
  const errors: string[] = [];
  const requiredScopes = new Set<string>();

  for (const row of minimumScopeMatrix) {
    if (registration.operation_families.includes(row.operation_family)) {
      for (const scope of row.minimum_delegated_scopes) requiredScopes.add(scope);
      if (row.delegated_only && registration.interaction_mode === "background_service") {
        errors.push(`M365_DELEGATED_ONLY_OPERATION:${row.operation_family}`);
      }
    }
  }

  if (registration.interaction_mode === "interactive_user") {
    if (authProfile.auth_mode === "oauth2_client_credentials" || authProfile.auth_mode === "api_key" || authProfile.auth_mode === "none") {
      errors.push("M365_FORBIDDEN_AUTH_MODE");
    }
    if (!registration.redirect_uri) {
      errors.push("M365_INTERACTIVE_REDIRECT_URI_REQUIRED");
    }
    if (authProfile.auth_mode === "oauth2_on_behalf_of" && registration.verified_obo_chain !== true) {
      errors.push("M365_OBO_CHAIN_UNVERIFIED");
    }
  }

  for (const scope of requiredScopes) {
    if (!registration.requested_scopes.includes(scope) && !authProfile.scopes.includes(scope)) {
      errors.push(`M365_SCOPE_MISSING:${scope}`);
    }
  }

  return errors;
}
```

##### Runtime auto-refresh rule

If runtime dispatch encounters an expired token and:
- `refresh_policy = "auto_if_refresh_token"`
- a refresh token ref exists
- and the profile is not in `revoked` state,

EC may attempt one automatic refresh before failing the dispatch.

If refresh succeeds:
- update `expires_at`, `refresh_at`, `status`, and write a receipt.

If refresh fails:
- set `auth_state = "challenge_required"` or `expired`,
- emit a receipt,
- and require user-visible re-auth.

##### Shared internal operation contracts

`M365OperationRequestSchema` and `M365OperationResponseSchema` are internal typed contracts consumed by DOC4 / DOC11 / OpenClaw dispatch layers. DOC3 must **not** create one public REST endpoint per M365 tool just because the operation contracts exist.

##### M365/MCP error codes added in this revision

Add all of the following canonical error codes:

- `M365_FORBIDDEN_AUTH_MODE`
- `M365_SCOPE_MISSING`
- `M365_OBO_CHAIN_UNVERIFIED`
- `M365_INTERACTIVE_REDIRECT_URI_REQUIRED`
- `M365_AUTH_EXPIRED`
- `M365_DELEGATED_ONLY_OPERATION`
- `M365_BACKGROUND_ONLY_OPERATION`
- `MCP_AUTH_CHALLENGE_REQUIRED`
- `MCP_AUTH_PROFILE_IN_USE`
- `MCP_SERVER_NOT_ACTIVE_IN_BUILD`
- `MCP_SERVER_DELETE_BLOCKED`
- `MCP_OAUTH_STATE_INVALID`
- `MCP_OAUTH_CALLBACK_EXPIRED`
- `MCP_OAUTH_REFRESH_NOT_ALLOWED`
- `MCP_OAUTH_TOKEN_EXCHANGE_FAILED`
- `MCP_ASK_FIRST_REQUIRED`
- `MCP_ASK_FIRST_EXPIRED`
- `MCP_SMOKE_TEST_FAILED`

---

#### §0.2F Acrobat adapter contract family (authoritative)

```ts
// packages/contracts/src/adapters/acrobat.ts
import { z } from "zod";

export const AcrobatCapabilitySchema = z.enum([
  "extract_text",
  "extract_tables",
  "ocr",
  "redaction_prep",
]);

export const AcrobatAdapterHealthSchema = z.object({
  adapter_state: z.enum(["healthy", "degraded", "disabled", "missing"]),
  supported_capabilities: z.array(AcrobatCapabilitySchema).default([]),
  reason_code: z.string().max(120).optional(),
  last_checked_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});

export const AcrobatSourceRefSchema = z.object({
  source_ref_kind: z.enum(["document_ref", "temp_artifact_ref", "materialized_bundle"]),
  source_ref: z.string().max(240),
});

export const AcrobatExtractTextRequestSchema = AcrobatSourceRefSchema.extend({
  capability: z.literal("extract_text"),
  page_range: z.string().max(120).optional(),
  ocr_fallback: z.boolean().default(false),
  schema_version: z.literal(1),
});

export const AcrobatExtractTablesRequestSchema = AcrobatSourceRefSchema.extend({
  capability: z.literal("extract_tables"),
  page_range: z.string().max(120).optional(),
  table_mode: z.enum(["auto", "strict", "layout_preserving"]).default("auto"),
  schema_version: z.literal(1),
});

export const AcrobatOCRRequestSchema = AcrobatSourceRefSchema.extend({
  capability: z.literal("ocr"),
  language_hint: z.string().max(40).optional(),
  page_range: z.string().max(120).optional(),
  schema_version: z.literal(1),
});

export const AcrobatRedactionPrepRequestSchema = AcrobatSourceRefSchema.extend({
  capability: z.literal("redaction_prep"),
  pattern_set: z.array(z.string().max(120)).default([]),
  page_range: z.string().max(120).optional(),
  schema_version: z.literal(1),
});

export const AcrobatOperationResponseSchema = z.object({
  operation_id: z.string().uuid(),
  capability: AcrobatCapabilitySchema,
  status: z.enum(["ok", "degraded", "blocked", "failed"]),
  output_ref: z.string().max(240).optional(),
  warning_codes: z.array(z.string().max(120)).default([]),
  receipt_id: z.string().uuid().optional(),
  error_code: z.string().max(120).optional(),
  error_message: z.string().max(500).optional(),
  schema_version: z.literal(1),
});
```

**Routes**
```http
GET  /api/adapters/acrobat/health
POST /api/adapters/acrobat/extract-text
POST /api/adapters/acrobat/extract-tables
POST /api/adapters/acrobat/ocr
POST /api/adapters/acrobat/redaction-prep
```

**Route request / response map**

| Route | Request schema | Success response |
|---|---|---|
| `GET /api/adapters/acrobat/health` | none | `AcrobatAdapterHealthSchema` |
| `POST /api/adapters/acrobat/extract-text` | `AcrobatExtractTextRequestSchema` | `AcrobatOperationResponseSchema` |
| `POST /api/adapters/acrobat/extract-tables` | `AcrobatExtractTablesRequestSchema` | `AcrobatOperationResponseSchema` |
| `POST /api/adapters/acrobat/ocr` | `AcrobatOCRRequestSchema` | `AcrobatOperationResponseSchema` |
| `POST /api/adapters/acrobat/redaction-prep` | `AcrobatRedactionPrepRequestSchema` | `AcrobatOperationResponseSchema` |

**Behavior rules**
1. Browser/Q must pass canonical `document_ref` or `temp_artifact_ref`, not raw local paths.
2. `extract_text`, `extract_tables`, and `ocr` may run in `enabled` or `ask_first` mode depending on data class.
3. `redaction_prep` is always treated as **ask-first** in DOC3.
4. If Acrobat is unavailable:
   - `GET /api/adapters/acrobat/health` must expose `adapter_state = "missing"` or `degraded`
   - action routes must fail with `ACROBAT_UNAVAILABLE`
   - Q must render the action as unavailable or degraded, not clickable success-looking UI.
5. If OCR is unsupported for the current file or adapter state, fail with `ACROBAT_OCR_UNSUPPORTED`.
6. If redaction prep is not approved, fail with `ACROBAT_REDACTION_PREP_REQUIRES_APPROVAL`.

**Error codes added in this revision**
- `ACROBAT_UNAVAILABLE`
- `ACROBAT_SOURCE_NOT_FOUND`
- `ACROBAT_OCR_UNSUPPORTED`
- `ACROBAT_EXTRACTION_FAILED`
- `ACROBAT_REDACTION_PREP_REQUIRES_APPROVAL`

---

### §0.2H MCP operational route contracts (authoritative)

```ts
// packages/contracts/src/mcp/ops.ts
import { z } from "zod";

export const MCPSmokeTestRequestSchema = z.object({
  server_id: z.string().max(120),
  project_id: z.string().max(160).optional(),
  include_tool_discovery: z.boolean().default(true),
  max_duration_ms: z.number().int().min(1000).max(60000).default(10000),
});

export const MCPSmokeTestResponseSchema = z.object({
  server_id: z.string().max(120),
  started_at: z.string().datetime(),
  completed_at: z.string().datetime(),
  overall_status: z.enum(["healthy", "degraded", "failed"]),
  auth_ok: z.boolean(),
  transport_ok: z.boolean(),
  tool_discovery_ok: z.boolean(),
  failure_reason_code: z.string().max(120).optional(),
  schema_version: z.literal(1),
});

export const MCPAuthChallengeRespondRequestSchema = z.object({
  challenge_id: z.string().uuid(),
  action: z.enum(["approve", "deny", "retry"]),
  selected_auth_profile_id: z.string().max(120).optional(),
});

export const MCPAuthChallengeRespondResponseSchema = z.object({
  challenge_id: z.string().uuid(),
  accepted: z.boolean(),
  next_step: z.enum(["retry_connection", "await_user_signin", "closed"]),
  schema_version: z.literal(1),
});
```
```

**And modify `CapabilityUseReceiptSchema` by adding:**

```ts
  auth_profile_id: z.string().max(120).optional(),
  identity_asserted_kind: z.enum(["local_user", "oauth_user", "service_principal", "unknown"]).default("unknown"),
  identity_asserted_ref: z.string().max(240).optional(),
```

**UI rule to add:**

```md
Q receipt cards for connector / MCP activity must display the identity assertion fields when present so the user can see whose permission grant was used.
```

---


### §0C.19 Canonical schema resolution table (authoritative deduplication)

Where inherited sections (Parts 1–4) define schemas that overlap with §0C or §0.2 canonical versions, the following table governs. **Inherited versions are historical reference only. Do not compile them. Do not import them. Do not use them as implementation targets.**

| Schema | Canonical file path | Authoritative section | Superseded locations (do NOT compile) |
|---|---|---|---|
| `LearnSessionSchema` | `packages/contracts/src/learning/learn-session.ts` | §0C.1 | Part 3 §0A.2 |
| `LearningEntryModeSchema` | `packages/contracts/src/learning/learn-session.ts` | §0C.1 | Part 3 §0A.2 `LearningEntryPointSchema` (different name, different values — SUPERSEDED) |
| `CreateLearnSessionRequestSchema` | `packages/contracts/src/learning/create-learn-session.ts` | §0C.2 | Part 3 §0A.3 (uses `entry_point` — SUPERSEDED) |
| `LearningRuntimeSnapshotSchema` | `packages/contracts/src/learning/runtime-snapshot.ts` | §0C.7 | — |
| `ObservationScopeRuntimeSchema` | `packages/contracts/src/learning/observation-scope-runtime.ts` | §0C.8 | — |
| `SemanticWorkflowInterpretationSchema` | `packages/contracts/src/learning/semantic-workflow-interpretation.ts` | §0C.9 (as updated by §0C.9A) | — |
| `ValidationRunSchema` | `packages/contracts/src/learning/validation-run.ts` | §0C.10 | — |
| `TestPolicySchema` | `packages/contracts/src/learning/test-policy.ts` | §0C.14 | — |
| `AbilityLookupQuerySchema` | `packages/contracts/src/abilities/lookup.ts` | §0C.11 | — |
| `ControlSurfaceSchema` | `packages/contracts/src/capabilities/control-surface.ts` | §0.2A | §1.1E (raw string version — SUPERSEDED) |
| `HybridActionPlanSchema` | `packages/contracts/src/capabilities/hybrid-action-plan.ts` | §0.2 canonical contracts | §1.1E (duplicate — SUPERSEDED) |
| `SkillImportStateSchema` | `packages/contracts/src/skills/import-state.ts` | §0.2B (10 states) | §1.1P `ImportedSkillRecordSchema.stage_state` (7 states — SUPERSEDED) |
| `PortableSkillFrontmatterSchema` | `packages/contracts/src/skills/frontmatter.ts` | §0.2C (regex: `/^[a-z0-9][a-z0-9-]{1,63}$/`) | §1.1O (regex: `/^[a-z0-9-]{2,64}$/` — SUPERSEDED) |
| `MCPAuthProfileSchema` | `packages/contracts/src/mcp/auth-profile.ts` | §0.2D | — |
| `MCPServerRegistryEntrySchema` | `packages/contracts/src/mcp/server-registry.ts` | §0.2D (fields: `provider`, `display_name`, `transport`) | §1.1R (fields: `title`, `mode`, `base_url` — SUPERSEDED) |
| `MCPConnectionHealthSchema` | `packages/contracts/src/mcp/health.ts` | §0.2D (includes `tool_health`, `backoff_until`) | §1.1R (different shape: `auth_ok`, `transport_ok` — SUPERSEDED) |
| `ConnectorPolicyDecisionSchema` | `packages/contracts/src/mcp/policy.ts` | §0.2E | — |
| `SkillMiningSettingsSchema` | `packages/contracts/src/skills/mining.ts` | §0.2F | — |
| `WorkflowTraceClusterSchema` | `packages/contracts/src/skills/mining.ts` | §0.2F | — |
| `SkillBundleProposalSchema` | `packages/contracts/src/skills/proposal.ts` | §0.2F extended with §0A.17 fields (`install_lane`, `draft_graph_ref`, `installing_private`, `quarantined` states) | §0.2F base version alone is incomplete |
| `ObservedActionEventSchema` | `packages/contracts/src/learning/observed-action.ts` | Part 3 §0A.9 (13 adapter kinds, `learn_session_id`, `control_surface`, `redaction_mode`) | §0.2F version (6 sources, no session link — SUPERSEDED) |
| `ProjectSourceBindingSchema` | `packages/contracts/src/projects/source-binding.ts` | §0.7 (array-based `sharepoint_refs`, `onedrive_refs`, `ambiguity_policy`) | §1.1T (scalar `onedrive_root`, `sharepoint_site_id` — SUPERSEDED) |
| `CapabilityUseReceiptSchema` | `packages/contracts/src/capabilities/receipts.ts` | §0.8 (includes `dispatch_id`, `operation_id`, `route_trace_id`) | §1.1U (different fields — SUPERSEDED) |
| `CapabilityRegistryBridgeEntrySchema` | `packages/contracts/src/capabilities/bridge-entry.ts` | §1.1C as carried in R11.1 (4-value `origin_owner`, `.datetime()` on `updated_at`, `.max(240)` on `metadata_ref`) | DOC10 R10 local copy (3-value `origin_owner`, no `.datetime()` — SUPERSEDED; DOC10 must import from contracts) |
| `ConfigurationIntentSchema` | `packages/contracts/src/common/configuration-intent.ts` | §0.2G | — |
| `TeachSessionSchema` | DEPRECATED — do not compile | §0B.10 deprecation rule | §1.1F (full schema still present — SUPERSEDED; map to LearnSession) |

**Coding-agent directive:** If you encounter a schema definition in Parts 1–4 that conflicts with this table, ignore the inherited version. Import only from the canonical file path listed above.

---

### §0C.20 Skill import, staging, trigger testing, and packs (authoritative)

**Skill import states**
```ts
// packages/contracts/src/skills/import-state.ts
import { z } from "zod";

export const SkillImportStateSchema = z.enum([
  "upload_pending",
  "uploaded",
  "scanning",
  "scan_failed",
  "scan_complete",
  "staging",
  "stage_failed",
  "ready_for_review",
  "install_queued",
  "install_failed",
  "installed_private",
  "approved",
  "rejected",
  "discarded",
  "expired",
]);

export const SkillImportTerminalStateSchema = z.enum([
  "installed_private",
  "approved",
  "rejected",
  "discarded",
  "expired",
]);
```

**Skill import record**
```ts
import { z } from "zod";
import { SkillImportStateSchema } from "../skills/import-state";

export const ImportedSkillRecordSchema = z.object({
  import_id: z.string().uuid(),
  source: z.enum(["manual_upload", "anthropic_bundle", "openclaw_bundle", "elnor_generated"]),
  skill_name: z.string().max(80),
  source_ref_kind: z.enum(["temp_artifact", "materialized_bundle", "generated_bundle", "legacy_path"]).default("temp_artifact"),
  source_ref: z.string().max(240),
  original_filename: z.string().max(240).optional(),
  compatibility_report_ref: z.string().max(240).optional(),
  stage_state: SkillImportStateSchema,
  retry_count: z.number().int().min(0).default(0),
  last_error_code: z.string().max(120).optional(),
  skill_pack: z.string().max(64).optional(),
  requires_adapter: z.boolean().default(false),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Compatibility, upload, trigger-test, and pack contracts**
```ts
import { z } from "zod";

export const SkillCompatibilityFindingSchema = z.object({
  code: z.string().max(120),
  severity: z.enum(["info", "warning", "error"]),
  message: z.string().max(1000),
  path_hint: z.string().max(240).optional(),
});

export const SkillCompatibilityReportSchema = z.object({
  compatible: z.boolean(),
  requires_adapter: z.boolean().default(false),
  rule_version: z.string().max(80),
  suggested_pack_id: z.string().max(64).optional(),
  findings: z.array(SkillCompatibilityFindingSchema).default([]),
  schema_version: z.literal(1),
});

export const SkillImportUploadMetadataSchema = z.object({
  source: z.enum(["manual_upload", "anthropic_bundle", "openclaw_bundle", "elnor_generated"]).default("manual_upload"),
  requested_pack_id: z.string().max(64).optional(),
  notes: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const TempArtifactRefSchema = z.object({
  temp_artifact_ref: z.string().max(240),
  artifact_kind: z.enum(["skill_bundle_zip", "skill_bundle_dir", "generated_bundle"]),
  original_filename: z.string().max(240).optional(),
  content_hash_sha256: z.string().regex(/^[a-f0-9]{64}$/),
  byte_size: z.number().int().min(1),
  created_at: z.string().datetime(),
  expires_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const SkillImportUploadResponseSchema = z.object({
  temp_artifact: TempArtifactRefSchema,
  accepted: z.boolean(),
  schema_version: z.literal(1),
});

export const SkillImportDeleteUploadResponseSchema = z.object({
  deleted: z.boolean(),
  temp_artifact_ref: z.string().max(240),
  schema_version: z.literal(1),
});

export const SkillImportScanRequestSchema = z.object({
  source: z.enum(["manual_upload", "anthropic_bundle", "openclaw_bundle", "elnor_generated"]),
  temp_artifact_ref: z.string().max(240).optional(),
  staged_upload_ref: z.string().max(240).optional(), // deprecated alias; normalized by EC
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportScanResponseSchema = z.object({
  import_record: ImportedSkillRecordSchema,
  compatibility_report: SkillCompatibilityReportSchema,
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const SkillImportStageRequestSchema = z.object({
  import_id: z.string().uuid(),
  requested_pack_id: z.string().max(64).optional(),
  stage_notes: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportStageResponseSchema = z.object({
  import_record: ImportedSkillRecordSchema,
  materialized_bundle_ref: z.string().max(240).optional(),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const SkillImportApproveRequestSchema = z.object({
  import_id: z.string().uuid(),
  approval_note: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportRejectRequestSchema = z.object({
  import_id: z.string().uuid(),
  reason: z.string().min(1).max(1000),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportInstallPrivateRequestSchema = z.object({
  import_id: z.string().uuid(),
  target_scope: z.enum(["private", "workspace"]).default("private"),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportDiscardRequestSchema = z.object({
  import_id: z.string().uuid(),
  reason: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportRetryRequestSchema = z.object({
  import_id: z.string().uuid(),
  retry_kind: z.enum(["scan", "stage", "install"]),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const SkillImportTerminalActionResponseSchema = z.object({
  import_record: ImportedSkillRecordSchema,
  receipt_id: z.string().uuid().optional(),
  saga_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const SkillImportDetailResponseSchema = z.object({
  import_record: ImportedSkillRecordSchema,
  compatibility_report: SkillCompatibilityReportSchema.optional(),
  partial_deployment: LearnPartialDeploymentStateSchema.optional(),
  proposal_id: z.string().uuid().optional(),
  warnings: z.array(z.string().max(240)).default([]),
  schema_version: z.literal(1),
});

export const SkillImportListResponseSchema = z.object({
  items: z.array(ImportedSkillRecordSchema).default([]),
  total: z.number().int().min(0),
  page: z.number().int().min(1),
  page_size: z.number().int().min(1).max(100),
  schema_version: z.literal(1),
});

export const SkillTriggerTestRequestSchema = z.object({
  import_id: z.string().uuid().optional(),
  ability_id: z.string().max(160).optional(),
  trigger_text: z.string().min(1).max(2000),
  project_id: z.string().max(160).optional(),
  context_excerpt: z.string().max(4000).optional(),
  schema_version: z.literal(1),
}).strict().refine(
  (value) => Boolean(value.import_id || value.ability_id),
  "Either import_id or ability_id is required",
);

export const SkillTriggerTestResponseSchema = z.object({
  matched: z.boolean(),
  candidate_ability_id: z.string().max(160).optional(),
  candidate_import_id: z.string().uuid().optional(),
  score: z.number().min(0).max(1).optional(),
  reasons: z.array(z.string().max(240)).default([]),
  rejection_reasons: z.array(z.string().max(240)).default([]),
  receipt_id: z.string().uuid().optional(),
  schema_version: z.literal(1),
});

export const SkillPackMetadataSchema = z.object({
  pack_id: z.string().max(64),
  title: z.string().max(120),
  description: z.string().max(500).optional(),
  compatibility_target: z.string().max(120).optional(),
  active_build_surface: z.boolean().default(true),
  schema_version: z.literal(1),
});

export const SkillPacksListResponseSchema = z.object({
  packs: z.array(SkillPackMetadataSchema).default([]),
  schema_version: z.literal(1),
});
```

**Routes**
```http
POST   /api/skills/import/uploads
DELETE /api/skills/import/uploads/:tempArtifactRef
POST   /api/skills/import/scan
POST   /api/skills/import/stage
POST   /api/skills/import/approve
POST   /api/skills/import/reject
POST   /api/skills/import/install-private
POST   /api/skills/import/discard
POST   /api/skills/import/retry
GET    /api/skills/import/:importId
GET    /api/skills/import/list
POST   /api/skills/trigger-test
GET    /api/skills/packs
```

**Route request / response map**
| Route | Request schema | Success response |
|---|---|---|
| `POST /api/skills/import/uploads` | multipart file `bundle` + JSON `metadata` parsed as `SkillImportUploadMetadataSchema` | `SkillImportUploadResponseSchema` |
| `DELETE /api/skills/import/uploads/:tempArtifactRef` | none | `SkillImportDeleteUploadResponseSchema` |
| `POST /api/skills/import/scan` | `SkillImportScanRequestSchema` | `SkillImportScanResponseSchema` |
| `POST /api/skills/import/stage` | `SkillImportStageRequestSchema` | `SkillImportStageResponseSchema` |
| `POST /api/skills/import/approve` | `SkillImportApproveRequestSchema` | `SkillImportTerminalActionResponseSchema` |
| `POST /api/skills/import/reject` | `SkillImportRejectRequestSchema` | `SkillImportTerminalActionResponseSchema` |
| `POST /api/skills/import/install-private` | `SkillImportInstallPrivateRequestSchema` | `SkillImportTerminalActionResponseSchema` |
| `POST /api/skills/import/discard` | `SkillImportDiscardRequestSchema` | `SkillImportTerminalActionResponseSchema` |
| `POST /api/skills/import/retry` | `SkillImportRetryRequestSchema` | `SkillImportTerminalActionResponseSchema` |
| `GET /api/skills/import/:importId` | none | `SkillImportDetailResponseSchema` |
| `GET /api/skills/import/list` | none | `SkillImportListResponseSchema` |
| `POST /api/skills/trigger-test` | `SkillTriggerTestRequestSchema` | `SkillTriggerTestResponseSchema` |
| `GET /api/skills/packs` | none | `SkillPacksListResponseSchema` |

**Multipart upload contract**
1. `POST /api/skills/import/uploads` must accept `multipart/form-data`.
2. The multipart parts are:
   - required file part: `bundle`
   - optional text/json part: `metadata`
3. `metadata` must deserialize to `SkillImportUploadMetadataSchema`.
4. Accepted browser-upload MIME types:
   - `application/zip`
   - `application/x-zip-compressed`
   - `application/octet-stream` **only** when filename extension is explicitly accepted below
5. Accepted browser-upload filename extensions:
   - `.zip`
   - `.skillbundle.zip`
6. Maximum upload size is **100 MB**.
7. Required upload error codes:
   - `SKILL_IMPORT_FILE_TOO_LARGE`
   - `SKILL_IMPORT_UNSUPPORTED_MIME`
   - `SKILL_IMPORT_UNSUPPORTED_EXTENSION`
   - `SKILL_IMPORT_ARCHIVE_REQUIRED`
   - `SKILL_IMPORT_UPLOAD_EXPIRED`

**Transition graph**
- `upload_pending -> uploaded`
- `uploaded -> scanning`
- `scanning -> scan_complete | scan_failed | expired`
- `scan_complete -> staging | discarded`
- `staging -> ready_for_review | stage_failed | discarded`
- `ready_for_review -> install_queued | approved | rejected | discarded`
- `install_queued -> installed_private | install_failed`
- `scan_failed -> scanning` only through `POST /api/skills/import/retry` with `retry_kind = "scan"`
- `stage_failed -> staging` only through `POST /api/skills/import/retry` with `retry_kind = "stage"`
- `install_failed -> install_queued` only through `POST /api/skills/import/retry` with `retry_kind = "install"`
- `discarded`, `approved`, `rejected`, `expired`, and `installed_private` are terminal for import-state purposes

**Canonical import-flow rules**
1. Browser/Q upload must terminate at EC and produce `temp_artifact_ref`.
2. Browser/Q must not pass raw local desktop file paths as the canonical import handle.
3. `staged_upload_ref` is accepted only as a deprecated alias and must be normalized to `temp_artifact_ref`.
4. Existing records that only contain legacy `input_path` must be migrated on first successful load to:
   - `source_ref_kind = "legacy_path"`
   - `source_ref = <old input_path>`
5. Q must not display raw `legacy_path` values back to the user.
6. After a successful stage/materialization, EC may rewrite `source_ref_kind` to `materialized_bundle` and update `source_ref`.
7. Temp artifacts expire after 24 hours if they are not staged.
8. `ready_for_review` does **not** expire into silent deletion. It is a parked review state; stale warnings apply but explicit user action is still required.
9. `DELETE /api/skills/import/uploads/:tempArtifactRef` must fail closed if the temp artifact is already bound to an in-progress stage/install operation.
10. All write routes in this subsection must support idempotency by `client_request_id` for at least 24 hours.

**Import migration / backfill rules**
1. Legacy `promoted_shared` is no longer a valid import terminal state.
2. Shared promotion now occurs only through the learned-ability approval/install path.
3. Legacy `cancelled` import records must backfill to `discarded`.
4. Legacy `needs_adapter` must backfill to:
   - `stage_state = "scan_complete"`
   - `requires_adapter = true`
   - `compatibility_report.requires_adapter = true`
   - and a `findings[]` entry explaining the adapter requirement.
5. Any backfill performed by this revision must emit a receipt visible in the import history/receipts surfaces.

**Retry / error / TTL rules**
- Repeated scan, stage, or install retries with the same `client_request_id` must resolve idempotently.
- After 3 failed retries of the same `retry_kind`, EC must surface `retry_limit_reached` warning text in the current-view model.
- If a temp artifact has <2 hours remaining before expiry, Q must show an `upload_expiring_soon` warning badge.
- If an import enters `expired`, Q must show `expired` and offer restart-from-upload; it must not show hidden recovery.

**UI**
- Q → **Import Skill Bundle** must:
  1. upload,
  2. obtain `temp_artifact_ref`,
  3. scan,
  4. render scan result before stage/approve/install actions are enabled.
- Required visible import states:
  - uploading
  - uploaded-awaiting-scan
  - scanning
  - scan-failed
  - scan-complete
  - staging
  - stage-failed
  - ready-for-review
  - install-queued
  - install-failed
  - installed-private
  - rejected
  - discarded
  - expired

### §0C.20B Deprecated route migration (authoritative)

| Deprecated route | Canonical replacement | Migration behavior |
|---|---|---|
| `POST /api/capabilities/teach/start` | `POST /api/learn/sessions` with `entry_mode: "demonstrating_skill"` | 308 redirect for 6 months, then 410 |
| `POST /api/capabilities/teach/cancel` | `POST /api/learn/sessions/:sessionId/cancel` | 308 redirect |
| `POST /api/capabilities/teach/finish` | `POST /api/learn/sessions/:sessionId/stop` | 308 redirect |
| `POST /api/capabilities/promote` | `POST /api/abilities/:abilityId/promote-shared` | 308 redirect |
| `POST /api/skills/mining/proposals/:id/questions` | `POST /api/learn/sessions/:sessionId/answer-questions` | 308 redirect |
| `POST /api/skills/mining/proposals/:id/answers` | `POST /api/learn/sessions/:sessionId/answer-questions` | 308 redirect |
| `POST /api/skills/mining/proposals/:id/test` | `POST /api/learn/proposals/:proposalId/validate` | 308 redirect |
| `POST /api/skills/mining/proposals/:id/install-experimental` | `POST /api/learn/proposals/:proposalId/install` | 308 redirect |
| `POST /api/skills/mining/proposals/:id/feedback` | `POST /api/learn/proposals/:proposalId/revise` | 308 redirect |
| `GET /api/skills/mining/proposals` | `GET /api/learn/proposals` | 308 redirect |

---

### §0C.21 Categories (authoritative)

Categories are durable, EC-owned, user-defined grouping objects. They are overlapping (any item can belong to multiple categories), color-coded, and used as browser/filter overlays across Actions & Abilities.

#### Schema

```ts
// packages/contracts/src/common/category.ts
import { z } from "zod";

export const CategorySchema = z.object({
  category_id: z.string().uuid(),
  title: z.string().max(80),
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#6366F1"),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const CategoryAssignmentSchema = z.object({
  category_id: z.string().uuid(),
  item_kind: z.enum([
    "task_template", "automation", "skill", "bundle",
    "connector", "proposal", "learned_ability",
  ]),
  item_id: z.string().max(160),
  assigned_at: z.string().datetime(),
});
```

#### Storage

```text
ELNOR_MEMORY/system/categories/categories_current.json    # Array of CategorySchema
ELNOR_MEMORY/system/categories/assignments.jsonl           # Append-only assignments
```

#### Routes

```http
GET    /api/categories                           # List all categories
POST   /api/categories                           # Create category
PUT    /api/categories/:categoryId               # Update title/color
DELETE /api/categories/:categoryId               # Delete category + all assignments
POST   /api/categories/:categoryId/assign        # Assign item to category
DELETE /api/categories/:categoryId/assign/:itemId # Remove assignment
GET    /api/categories/:categoryId/items          # List items in category
```

#### UI contract

Right-click on any eligible item (skill, task, automation, proposal, bundle, connector) must show:
```
┌──────────────────────┐
│ Add to Category…     │
├──────────────────────┤
│ ● Litigation (blue)  │
│ ○ Henderson (green)  │
│ ● Daily Tasks (gray) │
│ ──────────────────── │
│ + New Category       │
└──────────────────────┘
```
Checkmarks indicate current assignments. Clicking toggles assignment. "New Category" opens an inline name + color picker.

---

### §0C.22 Observation adapter contracts (authoritative — promoted from §0A.9)

The following schemas from the inherited §0A.9–§0A.14 are promoted to canonical status. The §0A versions are now historical reference only.

#### Observation adapter status

```ts
// packages/contracts/src/learning/observation-adapter.ts
import { z } from "zod";

export const ObservationAdapterKindSchema = z.enum([
  "browser_dom",
  "browser_snapshot",
  "accessibility_ui",
  "keyboard_shortcut",
  "text_entry",
  "mouse_click",
  "clipboard",
  "file_dialog",
  "process_launch",
  "terminal_command",
  "midi",
  "mcp_receipt",
  "wrapper_receipt",
]);

export const ObservationAdapterStatusSchema = z.object({
  adapter_id: z.string().max(80),
  kind: ObservationAdapterKindSchema,
  enabled: z.boolean(),
  healthy: z.boolean(),
  phase: z.enum(["phase_1", "phase_2", "phase_3"]),
  last_seen_at: z.string().datetime().optional(),
  last_error_code: z.string().max(120).optional(),
  schema_version: z.literal(1),
});
```

#### Phase tiering (authoritative)

| Phase | Adapters | Day-1 available |
|---|---|---|
| Phase 1 | `browser_dom`, `browser_snapshot`, `mcp_receipt`, `wrapper_receipt`, `terminal_command`, `process_launch` | Yes |
| Phase 2 | `keyboard_shortcut`, `file_dialog`, `midi` | No — show "adapter not available" |
| Phase 3 | `accessibility_ui`, `mouse_click` (semantic), `clipboard`, `text_entry` | No — show "adapter not available" |

Learning modes that require Phase 2+ adapters must surface `ADAPTER_UNAVAILABLE` and suggest alternative capture methods.

#### Observed action event (canonical version)

```ts
// packages/contracts/src/learning/observed-action.ts
import { z } from "zod";
import { ObservationAdapterKindSchema } from "./observation-adapter";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const ObservedActionEventSchema = z.object({
  event_id: z.string().uuid(),
  learn_session_id: z.string().uuid(),
  adapter_kind: ObservationAdapterKindSchema,
  app_family: z.string().max(80).optional(),
  window_title: z.string().max(200).optional(),
  action_label: z.string().max(240),
  control_surface: ControlSurfaceSchema.optional(),
  target_ref: z.string().max(240).optional(),
  before_state_ref: z.string().max(240).optional(),
  after_state_ref: z.string().max(240).optional(),
  caused_state_change: z.boolean().default(false),
  redaction_mode: z.enum(["none", "mask_text", "hash_only"]).default("mask_text"),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

#### Workflow boundary (canonical version)

```ts
// packages/contracts/src/learning/boundary.ts
import { z } from "zod";

export const WorkflowBoundarySchema = z.object({
  boundary_id: z.string().uuid(),
  learn_session_id: z.string().uuid(),
  kind: z.enum(["start", "pause", "resume", "stop", "cancel", "trim_start", "trim_end", "mark_goal", "mark_step"]),
  source: z.enum(["user_click", "hotkey", "system_inferred", "api_call"]),
  label: z.string().max(400).optional(),
  source_event_id: z.string().uuid().optional(),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

#### Privacy and retention (canonical rules)

1. Observation mode must be opt-in (explicit user action to enable).
2. Persistent UI banner must be visible while observation is active.
3. Raw observations are proposal inputs, NOT canonical memory.
4. `text_entry` adapter: `mask_text` by default. Escalation to `none` requires explicit per-session user consent.
5. `clipboard` adapter: capture event class only, NOT raw payload, by default.
6. Raw observation events are purged 7 days after session reaches terminal state (§0C.16D).
7. User may view and delete captured observations before proposal generation via `GET /api/learn/sessions/:id/events` + `DELETE /api/learn/sessions/:id` (which purges associated observation data).

---

### §0C.23 Skill materialization contract (authoritative)

When a proposal transitions to `installing_private` or `approved`, the install saga materializes actual skill files.

#### Target paths

| Artifact | Canonical path | Projected path |
|---|---|---|
| SKILL.md | `ELNOR_MEMORY/system/learning/installed/<skill-name>/SKILL.md` | `~/.openclaw/workspace/skills/<skill-name>/SKILL.md` |
| Capability manifest | `ELNOR_MEMORY/system/capabilities/manifests/<capability_id>.json` | `~/.openclaw/workspace/skills/<skill-name>/capabilities/<capability_id>.json` |
| Metadata sidecar | `ELNOR_MEMORY/system/capabilities/metadata/<capability_id>.json` | — (EC-internal only) |
| Support pack refs | `ELNOR_MEMORY/system/learning/installed/<skill-name>/support-packs.json` | `~/.openclaw/workspace/skills/<skill-name>/support-packs/` |
| Test artifacts | `ELNOR_MEMORY/system/learning/installed/<skill-name>/tests/` | `~/.openclaw/workspace/skills/<skill-name>/tests/` |

#### Materialization function

```ts
// apps/ec-service/src/learning/materialize-skill.ts

export interface MaterializeSkillInput {
  proposal: SkillBundleProposal;
  interpretation: SemanticWorkflowInterpretation;
  validation_report: SkillValidationReport;
  install_lane: "experimental_private" | "approved_workspace" | "shared_promoted";
  skill_name: string; // sanitized, namespace-safe
}

export interface MaterializeSkillOutput {
  canonical_path: string;
  projected_path: string;
  capability_id: string;
  ability_id: string;
  files_written: string[];
}

export async function materializeSkill(
  input: MaterializeSkillInput,
): Promise<MaterializeSkillOutput> {
  // 1. Generate SKILL.md from interpretation + checkpoints (checkpoint-oriented)
  // 2. Generate capability manifest from proposal + interpretation
  // 3. Write canonical files to ELNOR_MEMORY (EC single-writer)
  // 4. Project to OpenClaw workspace (derived copy, read-only for OpenClaw)
  // 5. Return paths and IDs for saga continuation
  throw new Error("SPEC_IMPLEMENTATION_REQUIRED");
}
```

#### Post-materialization saga

After `materializeSkill` succeeds, the install saga continues with:
1. Bridge rebuild (`buildCapabilityBridge`)
2. Availability snapshot refresh (`refreshAbilityAvailability`)
3. Event emission (`learn.install.completed`, `bridge.rebuild.completed`, `ability.snapshot.updated`)

If materialization fails, the saga compensates by marking the proposal `quarantined` and emitting `learn.install.failed`.

---

### §0C.24 Canonical storage paths for learning subsystem (authoritative)

Use these paths only. If inherited sections reference different paths, use these:

```text

### §0C.25 Acceptance tests — R11.2 consolidated (authoritative)

All acceptance tests from inherited §9, companion §0A.33, and §0.11C remain valid. The following additional tests are added in R11.2 and supersede any conflicting test in prior sections.

#### Schema and compilation tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-01 | Barrel export at `packages/contracts/src/index.ts` compiles with zero duplicate identifier errors | Schema deduplication |
| R11.2-02 | No schema in Parts 1-4 is imported by any module — only canonical paths from §0C.19 are used | Supersession enforcement |
| R11.2-03 | `LearningEntryPointSchema` from Part 3 is not importable from the barrel | entry_mode/entry_point resolution |

#### Learning lifecycle tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-04 | `idle → armed → capturing → paused → capturing → stopped → reviewing → drafting_proposal → testing_proposal → ready_for_review → approved` completes | Full happy path |
| R11.2-05 | `idle → testing_proposal` directly → rejected with `INVALID_LEARN_STATE_TRANSITION` | Invalid transition |
| R11.2-06 | Second `POST /api/learn/sessions` while first is `capturing` → `LEARN_SESSION_ALREADY_ACTIVE` | Concurrency constraint |
| R11.2-07 | `POST .../resume` transitions from `paused` to `capturing` | Resume route |
| R11.2-08 | `POST .../mark-goal` during `capturing` records boundary with `kind: "mark_goal"` | Goal marking |
| R11.2-09 | `POST .../mark-step` during `capturing` records boundary with `kind: "mark_step"` | Step marking |
| R11.2-10 | `POST .../trim` adjusts session boundaries | Trim |
| R11.2-11 | `POST .../generate-proposal` from `stopped` creates proposal and transitions to `drafting_proposal` | Proposal generation |
| R11.2-12 | `POST .../approve` transitions from `ready_for_review` to `approved` | Approval |
| R11.2-13 | `POST .../reject` transitions from `ready_for_review` to `rejected` | Rejection |
| R11.2-14 | Session in `armed` for >1 hour auto-cancels with `session_timeout` receipt | TTL enforcement |
| R11.2-15 | `testing_proposal → drafting_proposal` (revision loop) works after test failure | Revision path |
| R11.2-16 | `learn.session.cancelled` receipt emitted on cancel | Cancel receipt |
| R11.2-17 | SSE stream on `GET .../events` delivers lifecycle events in real time | Event streaming |

#### Checkpoint tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-18 | verify_checkpoint `reached` → receipt in `checkpoint_receipts.jsonl` | Receipt emission |
| R11.2-19 | verify_checkpoint `skipped` → DOC8 friction event | Friction wiring |
| R11.2-20 | verify_checkpoint `failed` → DOC9 repair candidate | Repair wiring |
| R11.2-21 | Agent completes skill without calling verify_checkpoint → success (voluntary) | Voluntary semantics |
| R11.2-22 | Proposal with `success_condition: "div-8472"` → lint fails CKP-001 | DOM selector rejection |
| R11.2-23 | Proposal with `success_condition: "(340, 220)"` → lint fails CKP-002 | Coordinate rejection |
| R11.2-24 | Proposal with all semantic checkpoints → lint passes | Clean pass |
| R11.2-25 | Proposal failing checkpoint lint cannot advance to `ready_for_review` | Gate enforcement |
| R11.2-26 | Generated SKILL.md contains checkpoint descriptions, not click sequences | Generation policy |
| R11.2-27 | WorkflowGraph is never in OpenClaw's system prompt | IR-only rule |

#### Later-use and ability tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-28 | Install saga: materialize → project → bridge rebuild → snapshot refresh (atomic) | Saga completeness |
| R11.2-29 | After saga, `availability_current.json` contains ability with `usable_now: true` | Snapshot freshness |
| R11.2-30 | `POST /api/abilities/lookup` with matching query returns ability with `score >= 0.15` | Lookup algorithm |
| R11.2-31 | Quarantined ability excluded from lookup results | Quarantine exclusion |
| R11.2-32 | `POST /api/abilities/:id/deactivate` → ability `usable_now` becomes false | Deactivation |
| R11.2-33 | `POST /api/abilities/:id/promote-shared` → install lane changes | Promotion |
| R11.2-34 | DOC10 queries snapshot and routes to learned skill on match | End-to-end later-use |
| R11.2-35 | Q Skills page shows newly promoted ability after `ability.snapshot.updated` | UI refresh |
| R11.2-36 | If install saga fails at step 2, proposal quarantined, previous bridge intact | Saga compensation |

#### Security and housekeeping tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-37 | No wrapper writes directly to `.json`/`.yaml`/`.ini` — all emit ConfigurationIntent | Single-writer wrappers |
| R11.2-38 | `legal_tables_configure set` emits ConfigurationIntent, not file write | Legacy scrub |
| R11.2-39 | M365 connector with `auth_mode: "api_key"` → rejected at registration | Auth enforcement |
| R11.2-40 | No M365 connector stores raw token in workspace files | Token prohibition |

#### Category tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-41 | Create category → assign skill → query items returns skill | Category CRUD |
| R11.2-42 | Skill belongs to multiple categories simultaneously | Overlapping |
| R11.2-43 | Delete category removes all assignments | Cascade delete |

#### Observation tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-44 | Phase 2 adapter requested but unavailable → `ADAPTER_UNAVAILABLE` error | Phase enforcement |
| R11.2-45 | Observation active → persistent banner visible in Q | Privacy indicator |
| R11.2-46 | Observation events purged 7 days after terminal state | Retention cleanup |

#### Deprecated route tests

| # | Test | Validates |
|---|------|-----------|
| R11.2-47 | `POST /api/capabilities/teach/start` → 308 redirect to `/api/learn/sessions` | Migration |
| R11.2-48 | `POST /api/learn/session/start` (singular) → 308 redirect | Route normalization |

---

### §0C.26 Cross-doc object ownership (authoritative)

| Object type | Owner doc | DOC3's role |
|---|---|---|
| `TaskTemplate` / `TaskInstance` | ELNOR Core / EC Core | DOC3 does not define schemas. DOC3 shared workflow graph may be used as IR if Core adopts it. |
| `AutomationDefinition` | ELNOR Core / EC Core | DOC3 does not define schemas. |
| `ConnectorDefinition` (runtime registration) | DOC11 / DOC4 | DOC3 owns MCP policy/registry schemas for connector configuration. DOC11 owns runtime registration. |
| `Skill` (installed) | DOC3 | DOC3 owns SKILL.md, capability manifest, metadata, and bridge entry. |
| `LearnedAbility` | DOC3 | DOC3 owns the full learning lifecycle, proposal, install, and availability. |
| `Category` | DOC3 (R11.2) | DOC3 owns category schema, CRUD, and assignment. |
| `Project` / `Matter` | ELNOR Core / DOC7 | DOC3 consumes project bindings but does NOT define project truth. |

DOC3 does not attempt to define task, automation, or connector lifecycle schemas. If the shared `WorkflowGraphSchema` is adopted by Core for tasks/automations, that adoption is a Core decision, not a DOC3 mandate.

---

### §0C.27 Q UI spec amendment requirement (authoritative)

The current operative Q Dashboard UI specification does NOT define the following surfaces required by DOC3:

| Required surface | DOC3 section | Status in Q UI spec |
|---|---|---|
| Actions & Abilities top-level area | §0B.2 | Missing |
| Abilities → Learn page ("Ability Learning") | §0B.3, §0B.8, §0C.15 | Missing |
| Learn New Ability cards | §0B.5, §0C.15 | Missing |
| Active Capability Learning tab | §0B.8 | Missing |
| Pending Abilities tab | §0B.7, §0C.6 | Missing |
| History tab | §0B.8 | Missing |
| Proposal review drawer | §0.9C | Missing |
| Observation mode banner/controls | §0C.22 | Missing |
| Checkpoint progress view | §0C.9B | Missing |
| Category assignment menu | §0C.21 | Missing |
| Ability availability panel | §0C.11 | Missing |
| Install saga progress | §0C.23 | Missing |

**Directive:** The Q UI spec must be amended before the Learn subsystem is considered shippable. Until amended, Q coding agents must build these surfaces from DOC3's contracts and wireframes (§0C.9B, §0C.15, §0C.21, §0C.22), using the route contracts in §0C.16A and the state definitions in §0C.1/§0C.1A/§0C.6.

---

### §0C.28 Final compile-safety and migration-hardening block (authoritative)

```ts
// packages/contracts/src/compat/aliases.ts
import { z } from "zod";
import {
  SessionActionResponseSchema,
  ProposalInstallRequestSchema,
  ProposalInstallResponseSchema,
  ProposalDetailResponseSchema,
} from "../learning/responses";
import { LearnTimelineEventSchema } from "../learning/timeline";
import { ValidationRunSchema } from "../learning/validation-run";
import { MCPServerListItemSchema, MCPServerListResponseSchema } from "../mcp/shared";
import { SkillPacksListResponseSchema } from "../skills/import";
import { OutlookTargetSchema } from "../mcp/m365";

export {
  LearnTimelineEventSchema as LearningTimelineEventSchema,
};

export {
  MCPServerListItemSchema as ConnectorListItemSchema,
  MCPServerListResponseSchema as ConnectorListResponseSchema,
};

export const LearnPartialDeploymentStateSchema = z.object({
  state: z.enum([
    "none",
    "availability_pending",
    "catalog_pending",
    "bridge_pending",
    "receipt_pending",
    "degraded",
  ]),
  blocking: z.boolean(),
  reason_code: z.string().max(120).optional(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const LearnSessionMutationResponseSchema = SessionActionResponseSchema; // deprecated alias; new code must use SessionActionResponseSchema

export const StartValidationRunRequestSchema = z.object({
  run_label: z.string().max(120).optional(),
  selected_tests: z.array(z.string().max(120)).default([]),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const ValidationRunResponseSchema = z.object({
  validation_run: ValidationRunSchema,
  schema_version: z.literal(1),
});

export const ApproveProposalRequestSchema = z.object({
  approval_scope: z.enum(["approved_workspace", "shared_promoted"]).default("approved_workspace"),
  approval_note: z.string().max(1000).optional(),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const RejectProposalRequestSchema = z.object({
  reason: z.string().min(1).max(1000),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const ProposalMutationResponseSchema = ProposalDetailResponseSchema;
export const InstallProposalRequestSchema = ProposalInstallRequestSchema;
export const InstallProposalResponseSchema = ProposalInstallResponseSchema;
export const SkillPackListResponseSchema = SkillPacksListResponseSchema; // deprecated alias; new code must use SkillPacksListResponseSchema

export const DOC3AdditionalErrorCodeSchema = z.enum([
  "CAPTURE_ALREADY_STOPPED",
  "CATEGORY_LIMIT_REACHED",
]);

export const OutlookMailSearchRequestSchema = z.object({
  target: OutlookTargetSchema,
  query: z.string().min(1).max(1000),
  page_size: z.number().int().min(1).max(100).default(25),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();

export const OutlookThreadFetchRequestSchema = z.object({
  target: OutlookTargetSchema,
  thread_id: z.string().max(240),
  client_request_id: z.string().max(120).optional(),
  schema_version: z.literal(1),
}).strict();
```

**Canonical naming / alias rule**
If any older patch text, tests, retained appendix material, or modules still use:
- `LearningTimelineEventSchema`
- `ConnectorListItemSchema`
- `ConnectorListResponseSchema`
- `LearnSessionMutationResponseSchema`
- `StartValidationRunRequestSchema`
- `ValidationRunResponseSchema`
- `ApproveProposalRequestSchema`
- `RejectProposalRequestSchema`
- `ProposalMutationResponseSchema`
- `InstallProposalRequestSchema`
- `InstallProposalResponseSchema`
- `LearnPartialDeploymentStateSchema`
- `SkillPackListResponseSchema`

they must be treated as aliases of the canonical schemas exported or defined above. No second divergent definition is allowed.

**Outlook nested-target compile rule**
Any request contract that previously used `.and(OutlookTargetSchema)` with sibling properties must be rewritten to use a plain object with a nested `target: OutlookTargetSchema` field. No active DOC3 contract may rely on `.and(OutlookTargetSchema)` for Outlook request composition after this revision.

**Legacy-route and legacy-block suppression**
Add the following exact note immediately before any stale teach-route block, obsolete MCP public-route block, duplicate control inventory, duplicate schema block, or duplicate read-surface table that survives during merge:

```md
> LEGACY / NON-OPERATIVE: retained only for historical reading and traceability. Do not implement from this block. Use the newer R3.2 authoritative contract sections instead.
```

**Active-scope audit rule**
No active DOC3 surface may remain prose-only. If a surface is active in DOC3, it must have:
- at least one route or typed internal contract,
- request and response schema,
- health/degraded semantics,
- and at least one acceptance-test row.

**Startup/backfill migration receipt rule**
Any EC startup migration or backfill triggered by this revision must emit a `migration_backfill` receipt and must update the relevant current-view file atomically.

At minimum this applies to:
- MCP server current views,
- MCP auth-profile current views,
- pending ask-first approvals,
- skill-import records migrated from `input_path`,
- import-state backfills from `cancelled`, `needs_adapter`, or `promoted_shared`,
- and any current-view regeneration for the Skills catalog or availability snapshot.

**POST-route migration rule**
Deprecated POST routes must never use `301`. DOC3 may only use:
- direct internal aliasing, or
- `308` when an external redirect is unavoidable.

**Category-route normalization rule**
The active remove-assignment path is:

```http
DELETE /api/categories/:categoryId/items/:itemKind/:itemId
```

Any inherited `/api/categories/:categoryId/assign/:itemId` or similar path is legacy-only and must not be implemented as the active route.

**Acceptance-test additions**
Append the following rows to the authoritative acceptance-test section:

| Test name | Required pass condition |
|---|---|
| `mcp_named_responses_consistent` | all authoritative MCP route rows, companion rows, and control rows use the same named request/response schemas |
| `mcp_shared_auth_profile_delete_blocked` | deleting an in-use auth profile returns `MCP_AUTH_PROFILE_IN_USE` |
| `mcp_server_delete_retains_shared_profile` | deleting one of multiple servers sharing a profile returns `retained_shared` |
| `mcp_ask_first_resume_correlation` | approve/deny preserves `correlation_id` across receipt + paused dispatch |
| `mcp_server_list_badge_truth` | connector list badges derive from backend fields only |
| `m365_auto_refresh_once` | one automatic refresh attempt occurs, then challenge state is surfaced if refresh fails |
| `acrobat_health_blocks_buttons` | Q shows degraded/unavailable state when Acrobat is missing or degraded |
| `acrobat_redaction_prep_ask_first` | redaction prep is blocked until approval is granted |
| `skill_import_retry_idempotent` | duplicate retry request with same `client_request_id` does not duplicate work |
| `skill_import_upload_expiring_warning` | uploads near TTL surface the warning state |
| `skill_import_routes_not_appendix_only` | import detail/list/trigger-test/packs routes and schemas exist in the active layer |
| `learn_read_routes_not_appendix_only` | sessions/proposals/history/receipts/detail/events routes exist in the active layer |
| `learn_inventory_uses_plural_session_routes` | all required Learn controls use `/api/learn/sessions/:sessionId/...` route families |
| `capture_already_stopped_error_rendered` | Stop capture shows the `CAPTURE_ALREADY_STOPPED` stale-state message and refresh action |
| `category_limit_reached_error_rendered` | category create/update surfaces `CATEGORY_LIMIT_REACHED` and disables optimistic local category creation |
| `legacy_post_redirects_never_use_301` | no deprecated POST route migration row or test case uses `301` |
| `watch_my_actions_alias_only` | any `Watch My Actions` label maps only to create-session observation mode and does not create a second runtime control |
| `appendix_legacy_blocks_prefixed` | every retained conflicting legacy route/control/schema block is prefixed with the non-operative banner |

### R11.2 partial deployment additions

| Component | If not deployed | Q behavior |
|---|---|---|
| `verify_checkpoint` tool | Learned skills function; checkpoints are informational metadata only | Show "checkpoint verification not yet available" in session detail |
| Checkpoint lint | Proposals advance with warning instead of lint gate | Show "checkpoint lint not available — manual review recommended" |
| DOC10 checkpoint_health routing | Route using existing health_status only | No change visible |
| Category CRUD routes | Categories not available | Hide "Add to Category" in right-click menu |
| SSE event stream | Q polls `GET .../detail` every 5 seconds as fallback | Show "real-time updates unavailable" indicator |
| Ability lookup | DOC10 uses generic capability routing only | Learned skills may not be discovered until lookup deploys |
| Observation adapters (Phase 2+) | Learning modes that need them show `ADAPTER_UNAVAILABLE` | Suggest alternative capture method |
| History route | History tab shows "coming soon" | Disable History tab |
| Ability activation/deactivation/quarantine routes | Abilities are managed only through proposal install/approve flow | Hide activate/deactivate/quarantine buttons |

**Hard release blockers (must be deployed before Learn is user-facing):**
1. `POST /api/learn/sessions` + `start`/`stop`/`cancel` routes
2. `POST /api/learn/proposals/:id/install`
3. Skill materialization (§0C.23)
4. Bridge rebuild trigger
5. Availability snapshot refresh
6. `GET /api/learn/runtime/current`
7. At least one Phase 1 observation adapter (browser_dom or wrapper_receipt)

Everything else may deploy incrementally.

---

---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 1 — Inherited Baseline — DOC3 App Skills R11 [Preserved Below]

# DOC3 App Skills — R11 [Canonical Consolidated]

## Revision Lineage (must persist in all later versions)

Based on:
- DOC3 App Skills — R10 [Consolidated Current]
- DOC3 Addendum Proposal R1 — Actions & Abilities, Capability Learning UX, Workflow Graphs, Categories, and Skill/Task Cohesion
- DOC3 Self-Learning / Guided Learning Patch Plan R2
- ELNOR First-Wave MCP Pack R3

This consolidated current version fully subsumes those prior operative versions and addenda for DOC3 scope.

## Consolidation Rule

This file is the current **single operative DOC3** specification.

Precedence in case of overlap is by **topic ownership**, not merely chronology:

1. **Inherited Baseline (R10)** governs all topics unless superseded below.
2. **Actions & Abilities Addendum** governs:
   - Actions / Abilities / Learn information architecture
   - browser/main-pane layout and navigation behavior
   - categories / collections / saved object organization
   - task vs skill vs automation vs bundle user-facing distinctions
3. **Self-Learning / Guided Learning Patch Plan R2** governs:
   - learning runtime semantics
   - LearnSession lifecycle
   - observation/teaching/clarification/proposal/test/install flows
   - later-use activation and learning receipts
   - workflow graph semantics where they relate to learning/runtime use
4. **First-Wave MCP Pack R3** governs:
   - the current DOC3-facing connector inventory
   - recommended rollout phases
   - default policy expectations per connector family
   - routing heuristics for first-wave providers

## Included Source Chain

- 1. Inherited Baseline — DOC3 App Skills — R10 [Consolidated Current]
- 2. Merged Addendum — Actions & Abilities / Learn IA and UX
- 3. Merged Addendum — Self-Learning / Guided Learning Patch Plan R2
- 4. Merged Appendix — ELNOR First-Wave MCP Pack R3

## Canonicalization Note

After this merge:
- the source addenda remain useful as historical/source artifacts,
- but they are **not** separate active operative DOC3 specs anymore.
- future red-team and implementation work should target **this file** as the operative DOC3.

---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 1 — Inherited Baseline — DOC3 App Skills R10

# DOC3 App Skills — R10 [Consolidated Current]

## Revision Lineage (must persist in all later versions)

Based on DOC3 App Skills R9, DOC3 Additions for LlamaIndex Retrieval Sidecar R1, and DOC3 App Skills R9.1 (Retrieval Provider and Topology Alignment). This consolidated current version fully subsumes those prior operative versions and addenda.


## Consolidation Rule

This file is the current single operative DOC3 app-skills specification for the retrieval/provider surfaces. Later merged revision blocks govern over earlier baseline statements on overlapping subjects.


## Included Source Chain

- 1. Inherited Baseline — DOC3 App Skills R9 — source file: `DOC3_App_Skills_R9.md`
- 2. Merged Addendum — DOC3 Additions for LlamaIndex Retrieval Sidecar R1 — source file: `DOC3_LlamaIndex_Integration_Additions_R1.md`
- 3. Merged Revision — DOC3 App Skills R9.1 (Retrieval Provider and Graph/Topology Alignment) — source file: `DOC3_App_Skills_R9_1_Retrieval_Provider_and_Topology_Alignment.md`



---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 1 — Inherited Baseline — DOC3 App Skills R9


# DOC3 App Skills R9

**Date:** 2026-03-09  
**Status:** Canonical revision — supersedes DOC3 App Skills R8. R7 had already merged the former DOC3 Patch R7 into a single normative document, R8 layered in portable skill bundles, MCP, and project-aware routing, and R9 now integrates the autonomous skill-mining additions plus the accepted red-team hardening changes into one normative document.  
**Companion documents:** DOC1, DOC2, DOC4, DOC7, DOC8, DOC9, DOC10, DOC11, DOC12, DOC14, DOC15, ELNOR Core canonical, Q UI Design canonical, and `DOC3_Companion_Doc_Delta_Plan_R6`.  
**Design stance:** OpenClaw-native first, EC single-writer, Gateway-first runtime truth, capability-learning additive rather than replacement, Anthropic/OpenClaw skill packaging adopted where compatible, MCP treated as a first-class connector layer, and autonomous skill mining permitted only through proposal / validation / install lanes.

**What changed in R9:**
- Preserved the full teach / trace / promote / bridge / health substrate introduced in R7.
- Added **portable AgentSkills-compatible skill packaging** and progressive disclosure guidance derived from the Anthropic skill-builder / agent-skills model, while keeping OpenClaw-native tools as the execution layer.
- Added **skill import, staging, linting, trigger testing, and skill-pack support** so ELNOR can ingest or generate reusable skills more easily.
- Added **MCP as a first-class control surface**, including:
  - client-side provider-native MCP support,
  - ELNOR-mediated MCP routing,
  - server-side custom ELNOR MCP servers,
  - per-provider / per-server / per-tool policy,
  - telemetry, ask-first approvals, and user controls.
- Added **project / matter cloud source-of-truth routing** so DOC7/Core project identity can resolve to OneDrive / SharePoint / Outlook / Teams / other live systems without creating a second rival “matter system.”
- Added a **Microsoft 365 MCP-first family**, plus Acrobat / Gmail / external AI-runtime guidance and a first-wave MCP pack linkage.
- Extended file layout, code modules, endpoints, UI surfaces, acceptance tests, and files-to-create accordingly.

**Architectural split preserved in R9:**
- OpenClaw owns native runtime execution, browser control, node/canvas/tool execution, and session truth.
- DOC11 remains the runtime-truth owner.
- DOC12 remains the visible room/multi-agent substrate.
- DOC4 `elnor-ec` remains the rich-memory / capsules / standing-orders / corrections / freshness bridge.
- DOC10 remains the capability-awareness / routing consumer, not the emitter.
- DOC7 Context Buckets remain additive support context.
- DOC8/DOC9/DOC15 remain learning / friction / repair / scoring systems.
- DOC3 remains the canonical owner of **app capability artifacts, skill packaging, connector-facing skill semantics, and hybrid control policy**.


## §0 R9 Normative Hardening, Autonomous Skill Mining, and MCP Operationalization

**Precedence rule.**  
This section is new in R9 and is **normative**. If any inherited section copied forward from R8 conflicts with §0, this section controls. The purpose of §0 is to:
- fold in the autonomous skill-mining addendum,
- resolve accepted red-team findings,
- remove coding-agent ambiguity,
- and make the DOC3 substrate implementable without drift.

### §0.1 Canonical contract ownership and shared package rule

All new DOC3 contracts introduced in R9 must live first in the shared contracts package and then be imported by DOC3-facing modules and companion docs.

**Required package root**
```text
packages/contracts/src/
```

**Required subtrees**
```text
packages/contracts/src/capabilities/
packages/contracts/src/mcp/
packages/contracts/src/skills/
packages/contracts/src/projects/
packages/contracts/src/common/
```

**Do not redefine locally**
- `CapabilityRegistryBridgeEntrySchema`
- `HybridActionPlanSchema`
- `ControlSurfaceSchema`
- `SkillImportStateSchema`
- `SkillBundleProposalSchema`
- `MCPAuthProfileSchema`
- `MCPServerRegistryEntrySchema`
- `MCPConnectionHealthSchema`
- `ConnectorPolicyDecisionSchema`
- `ConfigurationIntentSchema`

**Required barrel**
```ts
// packages/contracts/src/index.ts
export * from "./capabilities/bridge-entry";
export * from "./capabilities/control-surface";
export * from "./capabilities/hybrid-action-plan";
export * from "./capabilities/manifest";
export * from "./capabilities/metadata";
export * from "./capabilities/health";
export * from "./capabilities/trace";
export * from "./capabilities/teach";
export * from "./capabilities/receipts";
export * from "./mcp/auth-profile";
export * from "./mcp/server-registry";
export * from "./mcp/health";
export * from "./mcp/policy";
export * from "./mcp/route-decision";
export * from "./skills/import-state";
export * from "./skills/frontmatter";
export * from "./skills/proposal";
export * from "./skills/mining";
export * from "./projects/source-binding";
export * from "./common/error-envelope";
export * from "./common/configuration-intent";
```

### §0.2 Canonical schemas added or tightened in R9

#### §0.2A Control surfaces
```ts
// packages/contracts/src/capabilities/control-surface.ts
import { z } from "zod";

export const ControlSurfaceSchema = z.enum([
  "native_openclaw_tool",
  "native_openclaw_browser",
  "bridge_tool",
  "mcp_connector",
  "mcp_server",
  "app_api",
  "wrapper_script",
  "applescript",
  "python_wrapper",
  "keyboard_shortcut",
  "midi_binding",
  "page_knowledge_ui",
  "raw_ui",
]);
export type ControlSurface = z.infer<typeof ControlSurfaceSchema>;
```

#### §0.2B Skill import state machine
```ts
// packages/contracts/src/skills/import-state.ts
import { z } from "zod";

export const SkillImportStateSchema = z.enum([
  "scanned",
  "needs_adapter",
  "staged",
  "awaiting_review",
  "approved",
  "rejected",
  "installed_private",
  "promoted_shared",
  "failed",
  "cancelled",
]);

export const SkillImportTransitionSchema = z.object({
  import_id: z.string().uuid(),
  from_state: SkillImportStateSchema,
  to_state: SkillImportStateSchema,
  actor: z.enum(["system", "user", "reviewer", "repair_engine"]),
  reason: z.string().max(240),
  correlation_id: z.string().max(200),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

#### §0.2C Portable skill frontmatter (R9 additions)
```ts
// packages/contracts/src/skills/frontmatter.ts
import { z } from "zod";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const PortableSkillFrontmatterSchema = z.object({
  name: z.string().regex(/^[a-z0-9][a-z0-9-]{1,63}$/),
  title: z.string().max(120),
  description: z.string().max(500),
  version: z.string().max(32),
  triggers: z.array(z.string().max(200)).min(1),
  negative_triggers: z.array(z.string().max(200)).default([]),
  allowed_tools: z.array(z.string().max(80)).default([]),
  support_packs: z.array(z.string().max(120)).default([]),
  connector_hints: z.array(z.string().max(120)).default([]),
  depends_on: z.array(z.string().max(80)).default([]),
  preferred_control_surfaces: z.array(ControlSurfaceSchema).default([]),
  estimated_cost_class: z.enum(["free", "low", "medium", "high", "unknown"]).default("unknown"),
  assigned_namespace: z.string().regex(/^[a-z0-9][a-z0-9_-]{1,31}$/).optional(),
  portability_grade: z.enum(["direct", "adapter_required", "local_only", "non_portable"]).default("direct"),
  schema_version: z.literal(1),
});
```

#### §0.2D MCP auth + registry + health
```ts
// packages/contracts/src/mcp/auth-profile.ts
import { z } from "zod";

export const MCPAuthProfileSchema = z.object({
  auth_profile_id: z.string().max(120),
  provider: z.string().max(80),
  auth_mode: z.enum([
    "oauth2_delegated",
    "oauth2_on_behalf_of",
    "oauth2_client_credentials",
    "api_key",
    "none",
  ]).default("oauth2_delegated"),
  secret_ref: z.string().max(240).optional(),
  token_ref: z.string().max(240).optional(),
  token_subject_kind: z.enum(["user", "service", "delegated"]).default("user"),
  incremental_consent_supported: z.boolean().default(false),
  auth_challenge_supported: z.boolean().default(false),
  expires_at: z.string().datetime().optional(),
  refresh_at: z.string().datetime().optional(),
  status: z.enum(["healthy", "expired", "revoked", "unknown"]).default("unknown"),
  schema_version: z.literal(1),
});

// packages/contracts/src/mcp/server-registry.ts
export const MCPServerRegistryEntrySchema = z.object({
  server_id: z.string().max(120),
  provider: z.string().max(80),
  display_name: z.string().max(160),
  endpoint_url: z.string().url().optional(),
  transport: z.enum(["remote_http", "local_bridge", "provider_native"]),
  auth_profile_id: z.string().max(120).optional(),
  protocol_revision: z.string().max(40).optional(),
  tool_schema_hash: z.string().max(128).optional(),
  rate_limit_profile: z.enum(["unknown", "low", "medium", "high"]).default("unknown"),
  data_classes: z.array(z.string().max(80)).default([]),
  supported_tools: z.array(z.string().max(120)).default([]),
  enabled: z.boolean().default(true),
  schema_version: z.literal(1),
});

// packages/contracts/src/mcp/health.ts
export const MCPToolHealthSchema = z.object({
  ok: z.boolean(),
  last_error: z.string().max(240).optional(),
  last_checked_at: z.string().datetime().optional(),
});

export const MCPConnectionHealthSchema = z.object({
  server_id: z.string().max(120),
  health_status: z.enum(["healthy", "degraded", "disabled", "unknown"]),
  health_reason_code: z.string().max(120).optional(),
  last_checked_at: z.string().datetime().optional(),
  failure_count_rolling: z.number().int().min(0).default(0),
  backoff_until: z.string().datetime().optional(),
  tool_health: z.record(z.string().max(120), MCPToolHealthSchema).default({}),
  schema_version: z.literal(1),
});
```

#### §0.2E Connector policy decision + route scoring
```ts
// packages/contracts/src/mcp/policy.ts
import { z } from "zod";

export const ConnectorPolicyDecisionSchema = z.object({
  provider: z.string().max(80),
  server_id: z.string().max(120).optional(),
  tool_name: z.string().max(120).optional(),
  final_decision: z.enum(["allow", "deny", "ask_first"]),
  matched_rules: z.array(z.string().max(160)).default([]),
  precedence_trace: z.array(z.string().max(160)).default([]),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});

// packages/contracts/src/mcp/route-decision.ts
export interface ResolveMcpRouteInput {
  capabilityId: string;
  provider: string;
  actionClass: "read" | "write" | "admin";
  projectId?: string;
  desiredTool?: string;
  allowedServers: string[];
  askFirstServers: string[];
  registry: z.infer<typeof MCPServerRegistryEntrySchema>[];
  health: Record<string, z.infer<typeof MCPConnectionHealthSchema>>;
}
export interface ResolveMcpRouteResult {
  decision: "allow" | "deny" | "ask_first" | "degraded" | "no_route";
  serverId?: string;
  scoreBreakdown?: Array<{ server_id: string; score: number; reasons: string[] }>;
}
```

#### §0.2F Autonomous skill mining contracts
```ts
// packages/contracts/src/skills/mining.ts
import { z } from "zod";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const SkillMiningSettingsSchema = z.object({
  mode: z.enum(["off", "ask_before_build", "build_then_review", "auto_install_private"])
    .default("build_then_review"),
  observe_inside_elnor: z.boolean().default(true),
  observe_outside_elnor: z.boolean().default(false),
  min_successful_traces: z.number().int().min(1).default(3),
  require_user_feedback_before_build_when_ambiguous: z.boolean().default(true),
  require_tests_before_proposal: z.boolean().default(true),
  schema_version: z.literal(1),
});

export const WorkflowTraceClusterSchema = z.object({
  cluster_id: z.string().uuid(),
  normalized_goal: z.string().max(240),
  app_family: z.string().max(80),
  source_mode: z.enum(["elnor_observed", "user_guided", "desktop_observed"]),
  successful_trace_ids: z.array(z.string().uuid()).default([]),
  failed_trace_ids: z.array(z.string().uuid()).default([]),
  dominant_control_surfaces: z.array(ControlSurfaceSchema).default([]),
  variation_score: z.number().min(0).max(1).default(0),
  verification_coverage_score: z.number().min(0).max(1).default(0),
  project_specificity: z.enum(["global", "project_scoped", "unclear"]).default("unclear"),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const SkillBundleProposalSchema = z.object({
  proposal_id: z.string().uuid(),
  source_trace_ids: z.array(z.string().uuid()).min(1),
  source_mode: z.enum(["elnor_observed", "user_guided", "desktop_observed"]),
  draft_skill_path: z.string().max(240),
  draft_manifest_path: z.string().max(240),
  proposal_state: z.enum([
    "draft",
    "awaiting_user_input",
    "testing",
    "ready_for_review",
    "installed_private",
    "approved",
    "rejected",
    "cancelled",
  ]),
  ambiguity_flags: z.array(z.string().max(160)).default([]),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const ObservedActionEventSchema = z.object({
  event_id: z.string().uuid(),
  source: z.enum(["browser", "desktop_app", "shortcut", "midi", "wrapper", "mcp"]),
  app_family: z.string().max(80),
  action_type: z.string().max(120),
  action_summary: z.string().max(240),
  project_id: z.string().max(160).optional(),
  timestamp: z.string().datetime(),
  schema_version: z.literal(1),
});

export const SkillProposalFeedbackSchema = z.object({
  proposal_id: z.string().uuid(),
  feedback_kind: z.enum(["clarification", "edit", "approve", "reject", "request_tests", "install_private"]),
  feedback_text: z.string().max(2000).optional(),
  edited_fields: z.record(z.string(), z.unknown()).default({}),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

#### §0.2G Configuration-intent single-writer enforcement
```ts
// packages/contracts/src/common/configuration-intent.ts
import { z } from "zod";

export const ConfigurationIntentSchema = z.object({
  intent_id: z.string().uuid(),
  target: z.enum(["skill_config", "user_preference", "support_pack_binding", "connector_policy"]),
  capability_id: z.string().max(160).optional(),
  payload: z.record(z.string(), z.unknown()),
  requested_by: z.enum(["wrapper", "ui", "repair", "promotion", "skill_mining"]),
  correlation_id: z.string().max(200),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0.3 Manifest projection, bridge rebuild, and single-writer ownership

**Canonical rule**
- Canonical capability manifests live at:
  - `ELNOR_MEMORY/system/capabilities/manifests/<capability_id>.json`
- OpenClaw workspace manifest copies are derived projections:
  - `~/.openclaw/workspace/skills/<skill>/capabilities/<capability_id>.json`
- Only EC may write canonical manifests.
- OpenClaw, wrappers, hooks, and scripts must treat projected manifests as **read-only inputs**.

**Required projection function**
```ts
// apps/ec-service/src/capabilities/project-manifest.ts
export interface ProjectManifestToWorkspaceInput {
  capabilityId: string;
  manifest: CapabilityManifest;
  workspaceSkillDir: string;
}

export async function projectManifestToOpenClawWorkspace(
  input: ProjectManifestToWorkspaceInput,
): Promise<{ projectedPath: string; fileHash: string }> {
  // 1. validate manifest
  // 2. write temp file
  // 3. fsync
  // 4. atomic rename into workspace capability path
  // 5. return projected path + hash
  throw new Error("SPEC_IMPLEMENTATION_REQUIRED");
}
```

**Bridge rebuild events**
```ts
export const CapabilityEventNameSchema = z.enum([
  "capability.manifest.created",
  "capability.manifest.updated",
  "capability.manifest.deleted",
  "capability.health.changed",
  "capability.quarantined",
  "capability.reprobe.succeeded",
  "skill.imported",
  "skill.promoted",
  "teach.session.created",
  "teach.session.cancelled",
  "teach.proposal.submitted",
  "mcp.health.changed",
  "mcp.policy.changed",
  "mcp.receipt.created",
  "browser.relay.detached",
]);
```

**Required bridge builder**
```ts
// apps/ec-service/src/capabilities/build-bridge.ts
export interface BuildCapabilityBridgeInput {
  manifests: CapabilityManifest[];
  runtimeInventory: RuntimeCapabilityInventory;
  runtimeChecks: RuntimeCapabilityCheckIndex;
  healthIndex: CapabilityHealthIndex;
  importIndex: ImportedSkillIndex;
  now: string;
}
export interface BuildCapabilityBridgeResult {
  entries: CapabilityRegistryBridgeEntry[];
  metadataFiles: Record<string, CapabilityMetadataSidecar>;
  healthIndex: CapabilityHealthIndex;
}
export async function buildCapabilityBridge(
  input: BuildCapabilityBridgeInput,
): Promise<BuildCapabilityBridgeResult> {
  throw new Error("SPEC_IMPLEMENTATION_REQUIRED");
}
```

**Trigger policy**
- rebuild immediately on manifest/import/promotion/health-changing events
- nightly full rebuild as fallback
- fail closed: keep last-good bridge artifacts and set `bridge_state = stale`

### §0.4 Autonomous skill mining and user-guided skill building

R9 now makes autonomous skill mining a first-class part of DOC3.

#### §0.4A Goal
Allow ELNOR to:
- detect repeated successful workflows,
- optionally ask clarifying questions,
- draft a portable skill bundle and manifest,
- test the bundle,
- and either stage it for review or install it privately if policy allows.

#### §0.4B Non-goals
- do **not** silently promote generated skills to canonical shared skills
- do **not** allow observed outside-ELNOR activity without explicit observation mode
- do **not** bypass EC single-writer or OpenClaw-native runtime ownership

#### §0.4C Learning modes
1. **inside ELNOR**
   - trace repeated ELNOR-observed tasks
2. **outside ELNOR**
   - optional observation mode for desktop/browser/shortcut/MIDI actions
3. **user-guided**
   - explicit Teach Elnor / Learn From This

#### §0.4D Build modes
- `ask_before_build`
- `build_then_review`
- `auto_install_private`

#### §0.4E Install lanes
- `pending`
- `experimental_private`
- `approved_workspace`
- `shared_promoted`

#### §0.4F Required build gates
ELNOR may initiate a `skill_bundle_proposal` only when:
- at least `min_successful_traces` exist,
- variation score is below configured threshold,
- verification coverage is above configured threshold,
- no critical safety failures exist in the cluster,
- no higher-priority friction block is active,
- and the workflow is specific enough to be reusable.

**Strong positive indicators**
- repeated manual user praise / “use this again”
- repeated same goal across projects
- strong reduction in steps / friction after wrapper/MCP path selected
- stable control-surface order
- strong post-install or replay adoption

**Negative indicators**
- workflow is one-off or project-unique with no reusable core
- high ambiguity in naming or trigger language
- repeated manual overrides
- repeated failures on verification
- unresolved policy or dependency failures

#### §0.4G Pre-build clarification workflow
When ambiguity remains and `ask_before_build` or `require_user_feedback_before_build_when_ambiguous` is active, ELNOR must ask up to 5 concise clarification questions.

**Question categories**
- what should this skill be called?
- when should ELNOR use it?
- when should ELNOR NOT use it?
- is it project-specific or generally reusable?
- does it require approval every time?

**Required route**
```http
POST /api/skills/mining/proposals/:proposalId/questions
POST /api/skills/mining/proposals/:proposalId/answers
```

#### §0.4H Post-build review + revise-with-Elnor workflow
After the draft bundle is built and tested, Q must show:
- generated summary
- triggers / negative triggers
- support packs
- preferred control surfaces
- MCP/local dependencies
- test report
- collision warnings
- portability grade
- install lane

Required actions:
- `Revise with Elnor`
- `Install privately`
- `Approve to workspace`
- `Reject`
- `Request adapter`
- `Request more tests`

#### §0.4I Observation sources and privacy boundaries
Allowed observation sources when observation mode is enabled:
- OpenClaw browser traces
- wrapper execution traces
- MCP receipts
- local desktop app events (future gated)
- shortcuts / MIDI trigger logs (future gated)

**Privacy / safety rules**
- observation mode must be explicitly toggled on
- persistent banner must remain visible while active
- raw observed events are not canonical memory
- sensitive content must be redacted where possible
- observed events are proposal inputs only, not automatic installs by default

### §0.5 Skill packaging, import hardening, and progressive disclosure

#### §0.5A Packaging doctrine
R9 keeps Anthropic/OpenClaw-style packaging:
- `SKILL.md`
- `capabilities/*.json`
- `references/`
- `schemas/`
- `assets/`
- `support-packs.json`
- optional `scripts/`

**Progressive disclosure**
- Level 1: frontmatter only
- Level 2: concise `SKILL.md`
- Level 3: references/schemas/assets loaded only as needed

Do **not** keep giant JSON parameter docs in the hot-path `SKILL.md`.

#### §0.5B Namespacing and collision protection
Imported/generated skills must compile into runtime-safe namespaces.

**Runtime naming example**
- human name: `outlook-attachment-to-project-folder`
- namespace: `ext_ab12_outlook`
- runtime tool: `ext_ab12_outlook__save_attachment`

**Collision rule**
If `tool_shadowing_check = collision_detected`, the skill cannot install beyond `needs_adapter` or `pending`.

#### §0.5C Import transitions
Required transitions:
- `scanned -> needs_adapter | staged | failed`
- `needs_adapter -> staged | rejected | cancelled`
- `staged -> awaiting_review | installed_private | failed`
- `awaiting_review -> approved | rejected | cancelled`
- `approved -> promoted_shared | installed_private | failed`

### §0.6 MCP as an operational connector layer

#### §0.6A Route-class rule
R9 makes the “local-native vs cloud-native” distinction explicit.

**For local app / local file work**
`native_openclaw_tool` → `wrapper_script` / `shortcut` / `midi` → `page_knowledge_ui` → `raw_ui`

**For cloud system-of-record work**
`mcp_connector` / `provider_native` / `structured_connector` → browser automation

**For hybrid workflows**
- project identity resolved by Core/DOC7
- live fetch via MCP/connector
- local post-processing via skill or wrapper if needed

#### §0.6B Microsoft-specific rule
For cloud M365 retrieval/action:
- prefer MCP / Graph / structured connector
For local desktop Office behavior:
- AppleScript and local wrappers remain allowed
For local Word compare working-copy flow:
- keep local wrapper path

#### §0.6C MCP provider policy defaults
After a connector is installed and authorized:
- **read/search/list** = allowed by default unless policy overrides
- **create/update/delete/send** = `ask_first` by default
- user may override by:
  - provider
  - server
  - tool
  - project
  - session

#### §0.6D Required route scoring order
1. policy allowability
2. exact tool support
3. project binding relevance
4. health
5. transport preference
6. rate-limit/backoff state
7. deterministic tie-break by `server_id`

#### §0.6E Throttling/backoff
- 429/throttling => exponential backoff
- transient throttling must not immediately quarantine a server
- sustained repeated failures may degrade it
- UI must distinguish `throttled` from `degraded`

### §0.7 Project resolver, work objects, and source-of-truth rules

R9 confirms that project/matter/work-object semantics stay in Core/DOC7.

DOC3 must **not** create a second rival matter system.

**Allowed in DOC3**
- source bindings
- alias projections
- support-pack links
- preferred document class hints
- connector hints

**Not allowed in DOC3**
- canonical client/project/matter truth
- canonical status/deadline records
- independent work-object lifecycle

**Required schema**
```ts
// packages/contracts/src/projects/source-binding.ts
import { z } from "zod";

export const ProjectSourceBindingSchema = z.object({
  project_id: z.string().max(160),
  aliases: z.array(z.string().max(160)).default([]),
  bucket_ids: z.array(z.string().max(160)).default([]),
  preferred_doc_types: z.array(z.string().max(80)).default([]),
  sharepoint_refs: z.array(z.string().max(240)).default([]),
  onedrive_refs: z.array(z.string().max(240)).default([]),
  teams_refs: z.array(z.string().max(240)).default([]),
  source_of_truth: z.enum(["doc7_bucket", "core_project", "sharepoint", "onedrive", "mixed"]),
  ambiguity_policy: z.enum(["return_all", "require_disambiguation", "use_room_context_then_prompt"])
    .default("use_room_context_then_prompt"),
  schema_version: z.literal(1),
});
```

**Required resolver**
```ts
// apps/ec-service/src/capabilities/project-resolver.ts
export interface ResolveProjectSourceBindingInput {
  query: string;
  roomId?: string;
  activeProjectId?: string;
  candidateBucketIds?: string[];
}
export interface ResolveProjectSourceBindingResult {
  matches: Array<z.infer<typeof ProjectSourceBindingSchema> & { confidence: number }>;
  resolution_state: "resolved" | "ambiguous" | "none";
}
export async function resolveProjectSourceBinding(
  _input: ResolveProjectSourceBindingInput,
): Promise<ResolveProjectSourceBindingResult> {
  throw new Error("SPEC_IMPLEMENTATION_REQUIRED");
}
```

### §0.8 Receipts, audit controls, and observability

All capability and connector actions must emit correlated receipts.

**Required fields**
```ts
// packages/contracts/src/capabilities/receipts.ts
import { z } from "zod";

export const CapabilityUseReceiptSchema = z.object({
  receipt_id: z.string().uuid(),
  capability_id: z.string().max(160),
  provider: z.string().max(80).optional(),
  connector_server_id: z.string().max(120).optional(),
  dispatch_id: z.string().max(200).optional(),
  operation_id: z.string().max(200).optional(),
  route_trace_id: z.string().max(200).optional(),
  approval_mode: z.enum(["autonomous", "standing_order", "ask_first", "manual_click"]).default("autonomous"),
  receipt_detail_level: z.enum(["minimal", "standard", "detailed"]).default("standard"),
  project_id: z.string().max(160).optional(),
  result: z.enum(["success", "failure", "denied", "cancelled"]),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Log retention**
- rotate `traces.jsonl`, `receipts.jsonl`, `import-events.jsonl`, `mcp-receipts.jsonl`
- archive instead of silently deleting
- retention follows EC canonical log policy

### §0.9 UI/UX requirements added in R9

#### §0.9A Legacy Teach Elnor panel (deprecated user-facing label; see R11.1 §0B.4 and §0B.5)
States:
- idle
- selecting traces
- no eligible traces
- ambiguous workflow / needs input
- building proposal
- testing proposal
- review ready
- failed
- cancelled

Required controls:
- select traces
- explain inferred workflow
- edit summary
- add constraints
- cancel
- build proposal
- build & test

#### §0.9B Learn From This
Required behavior:
- only enabled when eligible traces/observation data exist
- if ambiguous, open clarification panel
- panel asks:
  - skill name
  - use conditions
  - non-use conditions
  - project-specific vs general
  - approval mode

#### §0.9C Proposal review pane
Show:
- generated skill summary
- triggers / negative triggers
- preferred control surfaces
- support packs
- MCP/local dependencies
- test results
- collision warnings
- portability grade
- health defaults
- install lane

Actions:
- revise with Elnor
- install privately
- approve shared
- reject
- request adapter
- request more tests

#### §0.9D Connector settings page
Sections:
- provider toggles
- server list with health badges
- tool-level permissions
- policy precedence explanation
- auth profile status
- receipts / audit links
- "turn off MCP for provider" controls

Ask-first flow must show:
- provider
- server/tool
- action class
- data class
- why access is needed
- allow once / allow for session / allow for project / deny

#### §0.9E Observation indicator
If outside-ELNOR observation is enabled, Q must show a persistent banner and quick toggle in every relevant workspace view.

### §0.10 Required routes and modules added in R9

#### §0.10A EC routes
```http
GET  /api/capabilities/bridge
GET  /api/capabilities/health
GET  /api/capabilities/stats
POST /api/capabilities/teach/start
POST /api/capabilities/teach/cancel
POST /api/capabilities/teach/finish
POST /api/capabilities/promote
POST /api/capabilities/reprobe
POST /api/capabilities/quarantine/clear

POST /api/skills/import/scan
POST /api/skills/import/stage
POST /api/skills/import/approve
POST /api/skills/import/reject
POST /api/skills/import/install-private
GET  /api/skills/import/list

GET  /api/skills/mining/settings
POST /api/skills/mining/settings
POST /api/skills/mining/scan
POST /api/skills/mining/proposals/:proposalId/questions
POST /api/skills/mining/proposals/:proposalId/answers
POST /api/skills/mining/proposals/:proposalId/test
POST /api/skills/mining/proposals/:proposalId/install-experimental
POST /api/skills/mining/proposals/:proposalId/feedback
GET  /api/skills/mining/proposals

GET  /api/mcp/servers
GET  /api/mcp/health
GET  /api/mcp/receipts
POST /api/mcp/policy/update
POST /api/mcp/auth/challenge/respond
POST /api/mcp/smoke-test
```

#### §0.10B Route auth middleware
```ts
// apps/ec-service/src/middleware/require-capability-privilege.ts
import type { RequestHandler } from "express";

export interface RequireCapabilityPrivilegeOptions {
  action: "read" | "write" | "admin";
  surface: "skills" | "capabilities" | "mcp";
}

export function requireCapabilityPrivilege(
  _opts: RequireCapabilityPrivilegeOptions,
): RequestHandler {
  throw new Error("SPEC_IMPLEMENTATION_REQUIRED");
}
```

**Rules**
- read-only bridge/health routes: authenticated session required
- teach/import/install/policy routes: elevated permission required
- auth unavailable => fail closed

#### §0.10C Error envelope
```ts
// packages/contracts/src/common/error-envelope.ts
import { z } from "zod";

export const ApiErrorEnvelopeSchema = z.object({
  error: z.object({
    code: z.string().max(80),
    message: z.string().max(240),
    retryable: z.boolean().default(false),
    correlation_id: z.string().max(200).optional(),
  }),
});
```

#### §0.10D Repo paths to use
Use repo-accurate path names. R9 standardizes on `q-frontend`, not `q-web`.

Required modules:
```text
apps/ec-service/src/capabilities/build-bridge.ts
apps/ec-service/src/capabilities/write-bridge.ts
apps/ec-service/src/capabilities/project-manifest.ts
apps/ec-service/src/capabilities/resolve-availability.ts
apps/ec-service/src/capabilities/probe-health.ts
apps/ec-service/src/capabilities/trace-recorder.ts
apps/ec-service/src/capabilities/teach-session.ts
apps/ec-service/src/capabilities/promote-template.ts
apps/ec-service/src/capabilities/hybrid-planner.ts
apps/ec-service/src/capabilities/project-resolver.ts

apps/ec-service/src/skills/import/scan-bundle.ts
apps/ec-service/src/skills/import/lint-bundle.ts
apps/ec-service/src/skills/import/stage-bundle.ts
apps/ec-service/src/skills/import/materialize-bundle.ts
apps/ec-service/src/skills/import/namespace-tools.ts
apps/ec-service/src/skills/mining/candidate-miner.ts
apps/ec-service/src/skills/mining/draft-skill-bundle.ts
apps/ec-service/src/skills/mining/run-canary-tests.ts

apps/ec-service/src/mcp/auth/load-profile.ts
apps/ec-service/src/mcp/policy/evaluate-policy.ts
apps/ec-service/src/mcp/routing/resolve-route.ts
apps/ec-service/src/mcp/health/update-health.ts
apps/ec-service/src/mcp/receipts/write-receipt.ts
apps/ec-service/src/mcp/providers/openai.ts
apps/ec-service/src/mcp/providers/anthropic.ts
apps/ec-service/src/mcp/providers/codex.ts
apps/ec-service/src/mcp/providers/perplexity.ts
apps/ec-service/src/mcp/providers/m365.ts
apps/ec-service/src/mcp/providers/custom.ts

apps/ec-service/src/mcp-servers/project-server.ts
apps/ec-service/src/mcp-servers/knowledge-server.ts
apps/ec-service/src/mcp-servers/local-files-server.ts

apps/q-frontend/src/features/skills/SkillMiningSettingsPanel.tsx
apps/q-frontend/src/features/skills/PreBuildQuestionsModal.tsx
apps/q-frontend/src/features/skills/SkillProposalReviewDrawer.tsx
apps/q-frontend/src/features/skills/SkillImportWizard.tsx
apps/q-frontend/src/features/skills/SkillCompatibilityPanel.tsx
apps/q-frontend/src/features/connectors/ConnectorSettingsPage.tsx
apps/q-frontend/src/features/connectors/ConnectorSettingsPanel.tsx
apps/q-frontend/src/features/connectors/ConnectorHealthBadge.tsx
apps/q-frontend/src/features/connectors/MCPReceiptCard.tsx
apps/q-frontend/src/features/connectors/ConnectorReceiptDetailSheet.tsx
apps/q-frontend/src/features/capabilities/CapabilityReceiptList.tsx
apps/q-frontend/src/features/capabilities/CapabilityAwarenessCard.tsx
```

### §0.11 Partial deployment, migration, and acceptance rules

#### §0.11A Partial deployment behavior
If R9 DOC3 ships before companion docs:
- bridge entries may exist with `bridge_state = partial_deployment`
- UI must show explicit degradation
- generated skills cannot auto-promote if companion routes are missing
- MCP policy UI must disable writes if policy backend absent

#### §0.11B Migration/init steps
1. create `packages/contracts` and compile shared schemas
2. create capability artifact directories under `ELNOR_MEMORY/system/capabilities`
3. create migration to canonical manifest projection paths
4. create import-state and proposal-state stores
5. backfill last-known capabilities into canonical manifest storage
6. create connector registry + health stores
7. add Q feature flags for skill mining and MCP controls

#### §0.11C New acceptance tests required in R9
Add to the acceptance section:
- shared schema import consistency
- bridge rebuild after manifest change
- duplicate schema absence
- import collision detection
- tool namespacing
- ask-before-build mode
- build-then-review mode
- auto-install-private mode
- observation mode banner + off switch
- project ambiguity handling
- MCP throttling/backoff
- connector auth expiry
- partial deployment degraded states
- configuration-intent enforcement
- receipt correlation IDs present


## §1 App Skills Pattern

### §1.1 How OpenClaw Skills Work

An OpenClaw skill is primarily a **folder with a `SKILL.md` file**. The `SKILL.md` has YAML frontmatter (name, description, allowed tools) and markdown instructions that tell the agent how to accomplish tasks. The folder can also contain scripts, reference documents, templates, page-knowledge files, UI anchors, and capability manifests.

**Ordinary DOC3 app skills are instructions plus artifacts — not plugin packages.**
The agent reads the `SKILL.md`, follows the instructions, and uses OpenClaw's built-in tools (`browser`, `exec`, `read`, `write`, `memory_search`, `node.invoke`, etc.) plus the DOC4 `elnor-ec` bridge tools when available.

**Important exception:** the DOC4 `elnor-ec` bridge is a dedicated bridge skill / bridge surface that may use bridge code and named `elnor_*` tools. That exception is intentional and does not change the packaging rule for ordinary DOC3 app skills.

**How the agent discovers and uses skills:**
1. OpenClaw scans skill folders and injects a compact list of available skills into the system prompt.
2. When the user request matches a skill description, the agent reads the full `SKILL.md`.
3. The agent follows the instructions in `SKILL.md`, calling wrapper scripts via native execution tools and using OpenClaw-native tools directly where appropriate.
4. The agent does not need to be told “use the Outlook skill” — OpenClaw skill matching handles discovery.

**Skill precedence (highest to lowest):**
- Workspace skills: `<workspace>/skills/`
- Managed/local skills: `~/.openclaw/skills/`
- Bundled skills

### §1.1A Capability kinds and ownership

DOC3 now treats each meaningful app/web action as a **capability artifact** that can be described, routed, tested, learned, promoted, degraded, repaired, or quarantined.

**Capability kinds**
- `native_openclaw_tool` — a capability already owned and executed by OpenClaw (for example browser snapshot / act loops, sessions, nodes, canvas, memory tools).
- `bridge_tool` — a named tool exposed through DOC4 `elnor-ec` and backed by EC HTTP APIs.
- `wrapper_script` — a tested AppleScript/Python/script-based execution surface used by a skill.
- `workflow_pattern` — a reusable multi-step pattern described in `SKILL.md` but not a separately callable runtime tool.
- `promoted_skill` — a learned/promoted capability bundle derived from ELNOR Core templates/process learning and materialized as DOC3 artifacts.

**Ownership rules**
- OpenClaw owns native runtime execution, native browser/session/tool behavior, native approvals, nodes, and native memory surfaces.
- DOC4 owns bridge-tool contracts for `elnor_*` capabilities.
- DOC3 owns app capability manifests, page knowledge, UI anchors, wrapper docs, control-surface policies, and app-facing promotion artifacts.
- DOC10 owns routing/read-model consumption of capability metadata.
- DOC11 owns runtime capability checks and runtime truth.
- EC is the single durable writer.

**What DOC3 does not do**
- It does not replace OpenClaw native tools.
- It does not let self-learning silently install arbitrary runtime code.
- It does not let Q, hooks, or wrapper scripts directly mutate canonical durable memory.
- It does not pretend a capability is usable “right now” unless runtime truth says so.

### §1.1B Capability manifest

Every routable or promotable capability in DOC3 must have a machine-readable manifest.

**File location**
```text
~/.openclaw/workspace/skills/<skill-name>/capabilities/<capability_id>.json
```

**Schema**
```ts
import { z } from "zod";

export const CapabilityKindSchema = z.enum([
  "native_openclaw_tool",
  "bridge_tool",
  "wrapper_script",
  "workflow_pattern",
  "promoted_skill",
]);

export const ControlSurfaceSchema = z.enum([
  "native_openclaw_browser",
  "native_openclaw_nodes",
  "native_openclaw_exec",
  "bridge_tool",
  "mcp_connector",
  "mcp_server",
  "app_api",
  "applescript",
  "python_wrapper",
  "keyboard_shortcut",
  "midi_binding",
  "page_knowledge_ui",
  "raw_ui",
]);

export const VerificationRuleSchema = z.object({
  kind: z.enum([
    "tool_result",
    "file_hash",
    "window_state",
    "snapshot_label",
    "api_state",
    "manual_confirmation",
  ]),
  target: z.string().max(240),
  expected: z.string().max(240).optional(),
});

export const HealthProbeSchema = z.object({
  kind: z.enum(["command", "tool", "browser", "noop"]),
  target: z.string().max(240),
  timeout_ms: z.number().int().positive().default(3000),
});

export const CapabilityManifestSchema = z.object({
  capability_id: z.string().max(160),
  family: z.string().max(80),
  kind: CapabilityKindSchema,
  title: z.string().max(200),
  origin_skill: z.string().max(120),
  aliases: z.array(z.string().max(80)).default([]),
  action_verbs: z.array(z.string().max(80)).default([]),
  surface_order: z.array(ControlSurfaceSchema).min(1),
  runtime_binding: z.object({
    binding_kind: z.enum(["native_tool", "bridge_tool", "script", "workflow_pattern"]),
    target: z.string().max(240),
  }),
  required_runtime_caps: z.array(z.string().max(120)).default([]),
  permissions: z.array(z.string().max(120)).default([]),
  requires_supervision: z.boolean().default(false),
  dry_run_supported: z.boolean().default(false),
  routing_eligible: z.boolean().default(true),
  verification: z.array(VerificationRuleSchema).min(1),
  health_probe: HealthProbeSchema,
  learning_policy: z.object({
    teach_mode_allowed: z.boolean().default(true),
    promotion_source_template_id: z.string().max(160).optional(),
    auto_capture_trace: z.boolean().default(true),
    proposal_required_for_mutation: z.boolean().default(true),
  }),
  schema_version: z.literal(1),
});
```

**Purpose of the manifest**
- lets DOC10 route capabilities without scraping prose,
- lets DOC11/DOC4/DOC3 resolve installed vs healthy vs usable-now state,
- lets teach-mode and repair pipelines target a concrete artifact,
- lets Q surface “what can I do here?” honestly.

### §1.1C Capability metadata and bridge export

Each capability also has a richer metadata sidecar used behind the compact bridge record.

**Metadata sidecar file**
```text
ELNOR_MEMORY/system/capabilities/metadata/<capability_id>.json
```

**Metadata schema**
```ts
import { z } from "zod";

export const CapabilityMetadataSchema = z.object({
  capability_id: z.string().max(160),
  summary: z.string().max(1000),
  source_refs: z.array(z.string().max(240)).default([]),
  control_surface_notes: z.array(z.object({
    surface: z.string().max(80),
    notes: z.string().max(500),
  })).default([]),
  dependency_refs: z.array(z.string().max(240)).default([]),
  page_knowledge_refs: z.array(z.string().max(240)).default([]),
  ui_anchor_refs: z.array(z.string().max(240)).default([]),
  bucket_refs: z.array(z.string().max(240)).default([]),
  known_failure_modes: z.array(z.object({
    trigger: z.string().max(240),
    recovery_ref: z.string().max(240),
  })).default([]),
  schema_version: z.literal(1),
});
```

**Canonical bridge output files**
```text
ELNOR_MEMORY/system/capabilities/bridge_entries_current.json
ELNOR_MEMORY/system/capabilities/metadata/<capability_id>.json
ELNOR_MEMORY/system/capabilities/health_current.json
ELNOR_MEMORY/system/capabilities/stats_current.json
ELNOR_MEMORY/system/capabilities/traces.jsonl
ELNOR_MEMORY/system/capabilities/teaching_sessions/<teach_session_id>.json
ELNOR_MEMORY/system/capabilities/ui_anchors.jsonl
```

**Compact bridge entry schema (consumed by DOC10)**  
R9 canonicalizes this schema in the shared contracts package. Do **not** re-declare it locally in DOC10 or runtime modules.

```ts
// packages/contracts/src/capabilities/bridge-entry.ts
import { z } from "zod";

export const CapabilityRegistryBridgeEntrySchema = z.object({
  capability_id: z.string().max(160),
  family: z.string().max(80),
  title: z.string().max(200),
  aliases: z.array(z.string().max(80)).max(20).default([]),
  action_verbs: z.array(z.string().max(80)).max(20).default([]),
  route_tier: z.number().int().min(1).max(5),
  requires_supervision: z.boolean().default(false),
  dry_run_supported: z.boolean().default(false),
  routing_eligible: z.boolean().default(true),
  health_status: z.enum(["healthy", "degraded", "quarantined", "disabled", "unknown"]),
  health_reason_code: z.string().max(120).optional(),
  origin_owner: z.enum(["doc3", "doc4_bridge", "openclaw_native", "provisional_scanner"]),
  built_in_openclaw: z.boolean().default(false),
  metadata_ref: z.string().max(240),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export type CapabilityRegistryBridgeEntry = z.infer<typeof CapabilityRegistryBridgeEntrySchema>;
```

**Bridge builder inputs**
- DOC3 capability manifests
- DOC3 metadata sidecars
- DOC4 runtime inventory / capability registry
- DOC11 runtime capability check output
- recent capability health/stats/traces
- ELNOR Core promotion bundles

### §1.1D Capability state model

A capability can be:
- **installed** — a manifest exists,
- **healthy** — recent probes and traces indicate it usually works,
- **usable now** — the current runtime, browser profile, session, permissions, and agent policy allow it right now.

Those are different.

**State schema**
```ts
import { z } from "zod";

export const CapabilityHealthStateSchema = z.object({
  capability_id: z.string().max(160),
  installed_state: z.enum(["present", "missing"]).default("present"),
  health_status: z.enum(["healthy", "degraded", "quarantined", "disabled"]),
  session_availability: z.enum(["supported", "degraded", "unsupported", "unknown"]).default("unknown"),
  reason_codes: z.array(z.string().max(80)).default([]),
  last_verified_at: z.string().datetime().optional(),
  cooldown_until: z.string().datetime().optional(),
  schema_version: z.literal(1),
});
```

**Why this matters**
- A capability may be installed and healthy in general but unavailable in the current room/session because the relay tab is detached, Word is not running, the wrong agent tool profile is in effect, or a node is disconnected.
- Q must show this honestly instead of promising ghost powers.

### §1.1E Hybrid control policy

Many of Will’s target apps are hybrid. Some actions are best done by API, some by wrapper, some by shortcut or MIDI, and only the leftovers should fall back to UI.

**Rule:** always prefer the most semantic control surface available for each sub-step.

**Default control-surface order**
1. native OpenClaw tool
2. bridge tool / structured connector
3. app API / wrapper script / AppleScript / Python wrapper
4. shortcut / MIDI / known semantic input surface
5. page-knowledge UI
6. raw UI

**HybridActionPlan schema**  
R9 canonicalizes this in the shared contracts package. Use `packages/contracts/src/capabilities/hybrid-action-plan.ts` and do not re-declare it in local app-family sections.

**Normative hybrid rules**
- no dual live execution for the same action path unless one side is read-only preflight;
- every write step must have a verification step;
- fallback use must be recorded in traces;
- supervision class must be explicit for irreversible/high-stakes actions.

### §1.1F Capability execution traces and teach mode

DOC3 capability learning is grounded in actual execution traces, not vibes.

**Trace schema**
```ts
import { z } from "zod";

export const CapabilityExecutionTraceSchema = z.object({
  trace_id: z.string().uuid(),
  capability_id: z.string().max(160),
  app_id: z.string().max(80),
  goal: z.string().max(500),
  session_key: z.string().max(200).optional(),
  room_id: z.string().max(200).optional(),
  agent_id: z.string().max(160).optional(),
  surfaces_attempted: z.array(z.string().max(80)).min(1),
  steps: z.array(z.object({
    step_id: z.string().max(80),
    action_label: z.string().max(240),
    surface: z.string().max(80),
    state_before_ref: z.string().max(240).optional(),
    state_after_ref: z.string().max(240).optional(),
    success: z.boolean(),
  })).min(1),
  verification_results: z.array(z.object({
    kind: z.string().max(80),
    passed: z.boolean(),
    detail: z.string().max(500).optional(),
  })).default([]),
  fallback_used: z.boolean().default(false),
  user_takeover: z.boolean().default(false),
  success: z.boolean(),
  failure_fingerprint: z.string().max(160).optional(),
  elapsed_ms: z.number().int().nonnegative(),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Teach-session schema**
```ts
import { z } from "zod";

export const TeachSessionSchema = z.object({
  teach_session_id: z.string().uuid(),
  capability_family_hint: z.string().max(80).optional(),
  source_surface: z.enum(["q_chat", "room", "task", "manual_import"]),
  room_id: z.string().max(200).optional(),
  source_message_ids: z.array(z.string().max(200)).default([]),
  selected_trace_ids: z.array(z.string().uuid()).default([]),
  proposed_capability_id: z.string().max(160).optional(),
  proposal_status: z.enum(["draft", "submitted", "approved", "rejected"]).default("draft"),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Teach-mode rules**
- “Teach Elnor” never writes a permanent new skill or manifest directly.
- Teach mode creates traces and then a **capability proposal bundle**.
- Proposal bundles go through EC approval / DOC9-style materialization.
- After approval and successful smoke tests, the promoted capability becomes routable.

### §1.1G UI anchors / page knowledge v2

Page knowledge is generalized into reusable UI anchors and page/app knowledge artifacts.

**File layout**
```text
skills/<skill-name>/page-knowledge/*.json
skills/<skill-name>/ui-anchors/*.json
```

**UI anchor schema**
```ts
import { z } from "zod";

export const UIAnchorRecordSchema = z.object({
  anchor_id: z.string().max(160),
  app_id: z.string().max(80),
  window_kind: z.string().max(120),
  semantic_role: z.string().max(120),
  locator_type: z.enum([
    "snapshot_label",
    "aria_text",
    "css_selector",
    "app_menu_path",
    "keyboard_shortcut",
    "midi_binding",
    "applescript_ref",
  ]),
  locator_value: z.string().max(400),
  alternates: z.array(z.string().max(400)).default([]),
  last_verified_at: z.string().datetime().optional(),
  confidence: z.number().min(0).max(1).default(0.5),
  failure_count: z.number().int().nonnegative().default(0),
  schema_version: z.literal(1),
});
```

**Rules**
- approved anchors live with the skill;
- drift corrections create EC proposals rather than silently mutating canonical files;
- browser and desktop app workflows both use the same anchor vocabulary where possible.

### §1.1H Promotion pipeline from ELNOR Core templates / process memory

ELNOR Core already has template maturity and promotion logic. DOC3 defines the **target artifacts** for that promotion.

**Promotion flow**
1. successful process/template patterns or teach traces create a candidate bundle;
2. candidate bundle includes:
   - manifest draft,
   - metadata sidecar,
   - optional `SKILL.md` patch,
   - optional wrapper/config/page-knowledge changes,
   - smoke-test plan;
3. candidate enters EC approval flow;
4. if approved, the materializer writes skill artifacts and triggers bridge rebuild;
5. capability remains `degraded` until smoke tests pass.

**What is not allowed**
- silent runtime-code installation from a teaching trace;
- bypass of EC single-writer discipline;
- bypass of approval for durable capability mutations.

### §1.1I Capability health, quarantine, and reprobe

DOC3 capabilities must degrade honestly.

**Default thresholds**
- 3 matching failures in 7 days → `degraded`
- 5 failures or one critical safety failure → `quarantined`
- 5 successful canaries after quarantine → `healthy`

**Health semantics**
- `healthy` — normal routing allowed
- `degraded` — routable with warning or only when strongly matched
- `quarantined` — not routable by default; visible in Q as unavailable/degraded
- `disabled` — intentionally off

**Reprobe**
- quarantined capabilities may be rechecked by a low-risk canary path;
- successful recheck updates health but does not delete prior failure traces.

### §1.1J Mutation boundaries and approval rules

This is a hard guardrail section.

**Allowed to change directly during normal execution**
- working copies
- transient trace files
- case-local outputs
- room-local artifacts
- noncanonical scratch files

**Not allowed to change directly**
- canonical `ELNOR_MEMORY`
- canonical capability manifests
- canonical metadata sidecars
- standing orders / corrections / permanent knowledge
- bridge entries

Those durable writes must go through EC / `elnor_learn` / proposal approval.

### §1.1K Context Buckets usage for skills

DOC7 Context Buckets are support context, not replacement capability objects.

**Use Context Buckets for**
- app manuals / quick refs
- shortcut registries
- MIDI maps
- UI cheat sheets
- runtime-health support context
- code-map / ops-map support for repair and teaching

**Do not use Context Buckets as**
- the canonical bridge registry,
- the canonical capability manifest store,
- or the durable proposal/approval store.

### §1.1L Native OpenClaw preference matrix

DOC3 must explicitly prefer OpenClaw-native ownership when a native surface already exists.

| Need | Preferred owner | DOC3 role |
|---|---|---|
| browser navigation/snapshot/act/evaluate | OpenClaw native browser tool | add page knowledge, guards, site workflows |
| sessions / subagents / room runtime truth | OpenClaw + DOC11 + DOC12 | consume and display honestly |
| node / canvas / local device surfaces | OpenClaw native nodes/canvas | add app-specific semantics only |
| native memory_search / memory_get | OpenClaw native memory tools | use as fallback or support tier only |
| rich capsules / corrections / standing orders / deadlines / freshness | DOC4 `elnor-ec` bridge backed by EC | consume, do not duplicate |
| cloud / SaaS / enterprise systems of record | MCP connector when healthy and allowed | route, constrain, and explain use |
| local desktop apps with semantic controls | local wrappers / app APIs / shortcuts / MIDI | keep MCP secondary unless intentionally wrapped |
| browser fallback when no good structured path exists | OpenClaw managed browser first, extension relay second | use only after semantic surfaces are exhausted |

### §1.1M Desktop automation / Peekaboo adapter contract

Peekaboo / desktop automation exists as a local automation substrate, not as the primary meaning layer.

**Intent**
- use desktop automation when wrapper/API/shortcut/browser-native paths cannot complete the workflow;
- persist what worked as traces/anchors/proposals rather than treating it as ephemeral improvisation.

**Adapter state schema**
```ts
import { z } from "zod";

export const DesktopAutomationAdapterStateSchema = z.object({
  adapter: z.enum(["peekaboo_bridge", "native_os_ui", "unknown"]),
  enabled: z.boolean().default(false),
  host_app: z.enum(["OpenClaw.app", "Peekaboo.app", "Claude.app", "unknown"]).default("unknown"),
  socket_path: z.string().max(240).optional(),
  permissions_state: z.object({
    accessibility: z.enum(["granted", "missing", "unknown"]).default("unknown"),
    screen_recording: z.enum(["granted", "missing", "unknown"]).default("unknown"),
  }),
  health_status: z.enum(["healthy", "degraded", "disabled"]).default("disabled"),
  last_verified_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});
```

**Rules**
- desktop automation cannot write canonical durable memory directly;
- it must emit traces, not final truths;
- capability proposals derived from desktop automation must pass through approval.


### §1.1N Q / room / Inbox surfaces for capability learning

DOC3 does not define the whole Q UI, but it must declare the user-facing surfaces its capability-learning artifacts are expected to power.

**Required surfaces**
- Deprecated UI alias note: legacy **Teach Elnor** actions must map to a `LearnSession`; default user-facing labels are generic.
- **Learn From This** action on a successful or failed workflow → selects traces for proposal creation
- **Inbox item: capability proposal** → shows manifest diff, control-surface diff, source traces, and approval actions
- **Capability awareness card / “What can I do here?”** → reads from the bridge and current runtime availability
- **Automation Health pill** → shows missing permissions, missing browser attachment, Word not running, relay detached, etc.
- **OpenClaw Integration view** → shows managed browser status, relay status, node status, bridge health, and smoke-test state
- **Learning timeline** → shows teaching sessions, promotions, repairs, quarantines, and successful reprobes

**Interaction rules**
- teach/proposal UI must never directly write canonical capability files;
- approval actions call EC / proposal routes;
- degraded or unavailable capability state must be shown explicitly, not hidden.


### §1.1O Portable AgentSkills-compatible skill bundles

DOC3 adopts an **AgentSkills-compatible** bundle shape so skills are easier to author, import, export, lint, and reuse across OpenClaw and Claude-style ecosystems.

**Why**
- Anthropic/OpenClaw-style skill bundles are much easier to create and inspect than ad hoc prompt blobs;
- OpenClaw already supports AgentSkills-compatible folder structures;
- ELNOR can use the bundle as the **portable package**, then layer richer manifests, health, and routing metadata beside it.

**Progressive disclosure rule**
1. frontmatter and lightweight metadata are always hot-path eligible;
2. `SKILL.md` is loaded when the skill is relevant;
3. large manuals, examples, screenshots, and support artifacts live in `references/`, `assets/`, or bucket-backed support packs and are loaded on demand.

**What this does not do**
- It does **not** replace DOC10’s capability bridge.
- It does **not** replace OpenClaw-native typed tools.
- It does **not** permit direct durable-memory mutation from imported skill bundles.

**Skill bundle compatibility lint schema**
```ts
import { z } from "zod";

export const PortableSkillFrontmatterSchema = z.object({
  name: z.string().regex(/^[a-z0-9-]{2,64}$/),
  description: z.string().min(20).max(400),
  version: z.string().regex(/^\d+\.\d+\.\d+$/).default("1.0.0"),
  skill_pack: z.string().regex(/^[a-z0-9-]{2,64}$/).optional(),
  tags: z.array(z.string().max(32)).default([]),
  triggers: z.array(z.string().max(160)).default([]),
  negative_triggers: z.array(z.string().max(160)).default([]),
  allowed_tools: z.array(z.string().max(160)).default([]),
  support_packs: z.array(z.string().max(120)).default([]),
  connector_hints: z.array(z.string().max(120)).default([]),
});

export const SkillLintFindingSchema = z.object({
  severity: z.enum(["info", "warn", "error"]),
  code: z.string().max(80),
  message: z.string().max(300),
  path: z.string().max(240).optional(),
});

export const SkillCompatibilityReportSchema = z.object({
  skill_id: z.string().max(80),
  compatible: z.boolean(),
  portability_grade: z.enum(["direct", "adapter_needed", "not_portable"]),
  findings: z.array(SkillLintFindingSchema).default([]),
  recommended_fixes: z.array(z.string().max(240)).default([]),
  schema_version: z.literal(1),
});
```

**Implementation modules**
```text
apps/ec-service/src/skills/parse-frontmatter.ts
apps/ec-service/src/skills/lint-portable-skill.ts
apps/ec-service/src/skills/grade-portability.ts
apps/q-frontend/src/features/skills/import/SkillCompatibilityPanel.tsx
```

**Required function signatures**
```ts
// apps/ec-service/src/skills/lint-portable-skill.ts
export async function lintPortableSkillBundle(inputDir: string): Promise<z.infer<typeof SkillCompatibilityReportSchema>> {
  const report = SkillCompatibilityReportSchema.parse({
    skill_id: "unknown",
    compatible: true,
    portability_grade: "direct",
    findings: [],
    recommended_fixes: [],
    schema_version: 1,
  });
  return report;
}
```

### §1.1P Skill import, staging, trigger testing, and packs

Imported or generated skills must enter a **staging** workflow before they become canonical workspace capabilities.

**Import states**
- `scanned`
- `staged`
- `lint_failed`
- `needs_adapter`
- `approved_for_workspace`
- `rejected`
- `promoted`

**Skill import record**
```ts
import { z } from "zod";

export const ImportedSkillRecordSchema = z.object({
  import_id: z.string().uuid(),
  source: z.enum(["manual_upload", "anthropic_bundle", "openclaw_bundle", "elnor_generated"]),
  skill_name: z.string().max(80),
  input_path: z.string().max(240),
  compatibility_report_ref: z.string().max(240),
  stage_state: z.enum([
    "scanned",
    "staged",
    "lint_failed",
    "needs_adapter",
    "approved_for_workspace",
    "rejected",
    "promoted",
  ]),
  skill_pack: z.string().max(64).optional(),
  requires_adapter: z.boolean().default(false),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Skill-pack manifest**
```ts
import { z } from "zod";

export const SkillPackManifestSchema = z.object({
  pack_id: z.string().regex(/^[a-z0-9-]{2,64}$/),
  title: z.string().max(120),
  description: z.string().max(400),
  skills: z.array(z.string().max(80)).default([]),
  default_support_packs: z.array(z.string().max(120)).default([]),
  recommended_connectors: z.array(z.string().max(120)).default([]),
  schema_version: z.literal(1),
});
```

**Routes**
```http
POST /api/skills/import/scan
POST /api/skills/import/stage
POST /api/skills/import/approve
POST /api/skills/import/reject
GET  /api/skills/import/:importId
POST /api/skills/trigger-test
GET  /api/skills/packs
```

**Request / response example**
```ts
// apps/ec-service/src/routes/skills.ts
import { z } from "zod";

export const SkillImportScanRequestSchema = z.object({
  source: z.enum(["manual_upload", "anthropic_bundle", "openclaw_bundle", "elnor_generated"]),
  input_path: z.string().max(240),
});

export const SkillImportScanResponseSchema = z.object({
  import_record: ImportedSkillRecordSchema,
  compatibility_report: SkillCompatibilityReportSchema,
});
```

**UI**
- Q → **Import Skill Bundle** button in Settings > Skills & Connectors
- Q → staging card showing:
  - compatibility grade,
  - missing tools/connectors,
  - trigger coverage,
  - actions: `Approve to Workspace`, `Request Adapter`, `Reject`

**Trigger-test rule**
- every imported or generated skill must have:
  - at least 3 positive trigger examples,
  - at least 3 non-trigger / negative examples,
  - at least 3 paraphrase examples.

### §1.1Q MCP as a first-class control surface

MCP is a first-class control surface for structured access to cloud services, internal services, and optional ELNOR-owned servers.

**Why**
- use MCP when the source of truth is a cloud / enterprise / internal service and a structured connector exists;
- avoid browser automation when a healthy structured connector can do the job;
- keep local desktop app control local-first unless the desktop function is intentionally wrapped as MCP.

**Control ordering update**
Preferred order for a remote/cloud task:
1. native OpenClaw or local semantic surface if the truth is local;
2. `mcp_connector` / `mcp_server` if the truth is remote and the connector is healthy and allowed;
3. app/API bridge tool;
4. browser page-knowledge workflow;
5. raw UI.

**Hybrid planner addition**  
R9 removes the duplicate local definition and points all callers to the shared contracts package.

```ts
// packages/contracts/src/capabilities/hybrid-action-plan.ts
import { z } from "zod";
import { ControlSurfaceSchema } from "./control-surface";

export const HybridActionPlanStepSchema = z.object({
  step_id: z.string().max(120),
  purpose: z.string().max(240),
  read_surface: ControlSurfaceSchema.optional(),
  act_surface: ControlSurfaceSchema,
  verify_surface: ControlSurfaceSchema,
  fallback_surfaces: z.array(ControlSurfaceSchema).default([]),
  supervision_class: z.enum(["none", "confirm_critical", "always_confirm"]).default("none"),
});

export const HybridActionPlanSchema = z.object({
  capability_id: z.string().max(160),
  goal: z.string().max(500),
  steps: z.array(HybridActionPlanStepSchema).min(1),
  schema_version: z.literal(1),
});
```

### §1.1R MCP registry, auth, policy, and provider controls

MCP support must be explicit, inspectable, and user-controllable.

**Registry entry**
```ts
import { z } from "zod";

export const MCPServerRegistryEntrySchema = z.object({
  server_id: z.string().regex(/^[a-z0-9-]{2,64}$/),
  title: z.string().max(120),
  mode: z.enum(["provider_native_remote", "elnor_managed_remote", "elnor_managed_local_bridge"]),
  base_url: z.string().url().optional(),
  provider_scope: z.array(z.enum(["openai", "anthropic", "codex", "openclaw", "other"])).default([]),
  exposed_tools: z.array(z.string().max(120)).default([]),
  auth_profile_id: z.string().max(120).optional(),
  data_class: z.enum(["public", "internal", "confidential", "privileged"]).default("internal"),
  default_state: z.enum(["enabled", "disabled", "ask_first"]).default("ask_first"),
  health_status: z.enum(["healthy", "degraded", "disabled", "unknown"]).default("unknown"),
  schema_version: z.literal(1),
});

export const MCPProviderPolicySchema = z.object({
  provider: z.enum(["openai", "anthropic", "codex", "openclaw", "other"]),
  allow_mcp: z.boolean().default(true),
  allowed_servers: z.array(z.string().max(64)).default([]),
  denied_servers: z.array(z.string().max(64)).default([]),
  ask_first_servers: z.array(z.string().max(64)).default([]),
  allowed_tools: z.array(z.string().max(120)).default([]),
  denied_tools: z.array(z.string().max(120)).default([]),
  schema_version: z.literal(1),
});

export const MCPConnectionHealthSchema = z.object({
  server_id: z.string().max(64),
  auth_ok: z.boolean().default(false),
  transport_ok: z.boolean().default(false),
  tool_discovery_ok: z.boolean().default(false),
  last_error_code: z.string().max(120).optional(),
  last_checked_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});
```

**Storage**
```text
ELNOR_MEMORY/system/mcp/registry_current.json
ELNOR_MEMORY/system/mcp/provider_policy_current.json
ELNOR_MEMORY/system/mcp/health_current.json
ELNOR_MEMORY/system/mcp/receipts.jsonl
```

**User controls**
- global toggle: `Allow MCP`
- per-provider toggle: `Allow OpenAI MCP`, `Allow Anthropic MCP`, `Allow Codex MCP`
- per-server state: `Enabled`, `Disabled`, `Ask First`
- per-tool override for sensitive actions

**Hard rules**
- connector auth secrets never live in skill bundles, Context Buckets, or canonical memory blobs;
- auth secrets live in runtime secret storage / environment / provider credential store;
- provider-native MCP must respect provider deny-lists;
- if policy and runtime disagree, the stricter policy wins.

### §1.1S Client-side MCP use, ELNOR-mediated MCP, and custom ELNOR servers

DOC3 supports both:
1. **provider-native MCP** where the runtime directly exposes an MCP tool surface; and
2. **ELNOR-mediated MCP** where ELNOR resolves intent and invokes the connector path on the user’s behalf.

**Default rule**
- For project/matter/work-object retrieval and sensitive operations, prefer **ELNOR-mediated MCP** so routing, policy, logging, and capability receipts stay coherent.
- Direct model-visible MCP can still be used for bounded, low-risk, well-scoped connector tasks when explicitly allowed.

**Runtime adapter record**
```ts
import { z } from "zod";

export const MCPRuntimeAdapterSchema = z.object({
  runtime: z.enum(["openai_responses", "anthropic_messages", "codex", "openclaw_worker", "other"]),
  supports_remote_mcp: z.boolean().default(false),
  supports_local_bridge: z.boolean().default(false),
  supports_tool_filtering: z.boolean().default(false),
  supports_server_oauth: z.boolean().default(false),
  supports_receipt_hooks: z.boolean().default(false),
  schema_version: z.literal(1),
});
```

**Implementation modules**
```text
apps/ec-service/src/mcp/registry.ts
apps/ec-service/src/mcp/policy.ts
apps/ec-service/src/mcp/health.ts
apps/ec-service/src/mcp/resolve-route.ts
apps/ec-service/src/mcp/emit-receipt.ts
apps/ec-service/src/mcp/project-resolver.ts
apps/ec-service/src/mcp/providers/openai.ts
apps/ec-service/src/mcp/providers/anthropic.ts
apps/ec-service/src/mcp/providers/codex.ts
apps/ec-service/src/mcp/servers/knowledge-server.ts
apps/ec-service/src/mcp/servers/project-server.ts
apps/ec-service/src/mcp/servers/local-files-server.ts
apps/q-frontend/src/features/connectors/ConnectorSettingsPanel.tsx
apps/q-frontend/src/features/connectors/MCPReceiptCard.tsx
```

**Required route-selection function**
```ts
// apps/ec-service/src/mcp/resolve-route.ts
import { z } from "zod";

const ResolveMcpRouteInputSchema = z.object({
  capability_id: z.string().max(160),
  provider: z.enum(["openai", "anthropic", "codex", "openclaw", "other"]),
  candidate_servers: z.array(MCPServerRegistryEntrySchema),
  policy: MCPProviderPolicySchema,
  project_hint: z.string().max(160).optional(),
});

export function resolveMcpRoute(input: z.infer<typeof ResolveMcpRouteInputSchema>) {
  const parsed = ResolveMcpRouteInputSchema.parse(input);
  const allowed = parsed.candidate_servers.filter((server) => {
    if (!parsed.policy.allow_mcp) return false;
    if (parsed.policy.denied_servers.includes(server.server_id)) return false;
    if (parsed.policy.allowed_servers.length > 0 && !parsed.policy.allowed_servers.includes(server.server_id)) return false;
    return server.health_status === "healthy" || server.health_status === "unknown";
  });

  return allowed[0] ?? null;
}
```

### §1.1T Project / matter resolver and cloud source-of-truth policy

DOC3 must not create a second rival matter system when DOC7/Core already hold project identity and support context.

**Rule**
- project / matter meaning stays in ELNOR Core + DOC7;
- MCP provides a live transport to source-of-truth systems like OneDrive / SharePoint / Outlook / Teams;
- buckets store summaries, aliases, policies, and pointers; connectors fetch live authoritative data.

**Resolver schema**
```ts
import { z } from "zod";

export const ProjectSourceBindingSchema = z.object({
  project_id: z.string().max(160),
  aliases: z.array(z.string().max(120)).default([]),
  bucket_ids: z.array(z.string().max(160)).default([]),
  onedrive_root: z.string().max(240).optional(),
  sharepoint_site_id: z.string().max(240).optional(),
  sharepoint_library_id: z.string().max(240).optional(),
  preferred_doc_types: z.array(z.string().max(80)).default([]),
  source_of_truth: z.enum(["onedrive_sharepoint", "local_files", "mixed", "unknown"]).default("unknown"),
  schema_version: z.literal(1),
});
```

**Resolver implementation**
```ts
// apps/ec-service/src/mcp/project-resolver.ts
import { z } from "zod";

export async function resolveProjectSourceBinding(projectId: string): Promise<z.infer<typeof ProjectSourceBindingSchema> | null> {
  if (!projectId) return null;
  return null;
}
```

**Usage rule**
For requests like “get the latest draft in this matter/project”:
1. resolve project identity from thread / room / bucket / explicit matter;
2. check source-of-truth binding;
3. if binding says OneDrive/SharePoint and connector is healthy, prefer MCP;
4. if not, use local/OpenClaw path;
5. browser fallback only if no structured path exists.

### §1.1U Telemetry, user controls, and receipts for skills and connectors

The user must be able to tell when skills, connectors, and MCP surfaces are being used.

**Receipt schema**
```ts
import { z } from "zod";

export const CapabilityUseReceiptSchema = z.object({
  receipt_id: z.string().uuid(),
  capability_id: z.string().max(160),
  route_surface: ControlSurfaceSchema,
  runtime: z.enum(["openai", "anthropic", "codex", "openclaw", "other"]),
  connector_server_id: z.string().max(64).optional(),
  project_id: z.string().max(160).optional(),
  action_class: z.enum(["search", "fetch", "read", "write", "update", "send", "schedule", "other"]).default("other"),
  data_class: z.enum(["public", "internal", "confidential", "privileged"]).default("internal"),
  approval_state: z.enum(["not_needed", "prompted", "granted", "denied"]).default("not_needed"),
  started_at: z.string().datetime(),
  completed_at: z.string().datetime().optional(),
  schema_version: z.literal(1),
});
```

**UI requirements**
- Q message footer / expandable receipt chip: `Used SharePoint MCP`, `Used Outlook Calendar MCP`, `Used local Word skill`, etc.
- Settings > Skills & Connectors:
  - connector states,
  - provider toggles,
  - recent MCP receipts,
  - auth/health repair actions.
- Inbox:
  - connector auth broken,
  - skill import needs adapter,
  - promotion candidate ready,
  - capability quarantined.

**Interaction states**
- `loading`: spinner + server name
- `populated`: connector badge, action summary, link to receipt detail
- `error`: connector unavailable / auth failed / denied by policy
- `disabled`: greyed badge with explanation and enable action if user is allowed to change it

### §1.2 Skill Structure

Every DOC3 skill may include some or all of the following:

```text
~/.openclaw/workspace/skills/{skill-name}/
├── SKILL.md
├── scripts/
│   ├── *.applescript / *.py / *.sh
├── references/
│   ├── manuals / structured docs / API notes / naming guides
├── assets/
│   ├── example screenshots / diagrams / templates / forms
├── knowledge/
│   ├── rules.md
│   ├── domain notes / support material
├── page-knowledge/
│   ├── *.json
├── ui-anchors/
│   ├── *.json
├── capabilities/
│   ├── <capability_id>.json
├── tests/
│   ├── trigger-tests.json
│   ├── smoke-tests.json
├── imports/
│   ├── import-metadata.json
├── support-packs/
│   ├── <pack-id>.json
├── control-surface.json
├── shortcut-registry.json         # optional
├── midi-registry.json             # optional
├── mcp-bindings.json              # optional
└── README.md
```

**Portable-skill rule**
- Use an AgentSkills-compatible folder shape and a conservative frontmatter subset so imported/exported skills remain as portable as practical across OpenClaw and Claude-style ecosystems.
- The `SKILL.md` remains the primary human-readable workflow document; detailed manuals and large reference materials belong in `references/` or bucket-backed support packs.

**There is no ordinary app-skill `skill.json` or `index.js` requirement in DOC3.**
The only deliberate bridge exception is DOC4 `elnor-ec`, which remains a special bridge surface and not the general packaging model for app skills.

### §1.3 SKILL.md format

Use a conservative, portable frontmatter subset modeled after Anthropic/OpenClaw AgentSkills-style bundles.

```markdown
---
name: outlook-mail-calendar
description: Search Outlook mail, retrieve message details, create or update calendar events, and explain when to use local Outlook wrappers versus Microsoft 365 connectors.
version: 1.0.0
skill-pack: office-pack
tags:
  - microsoft
  - mail
  - calendar
triggers:
  - search outlook mail
  - find email from a sender
  - check calendar availability
  - create calendar event
negative-triggers:
  - tweak local desktop window chrome
  - inspect a Bitwig plugin
allowed-tools:
  - Read
  - Bash(osascript:*)
  - Bash(python3:*)
  - browser.snapshot
  - browser.act
support-packs:
  - office-shortcuts
  - office-policies
connector-hints:
  - microsoft365-mail
  - microsoft365-calendar
---

# Outlook Integration

## When to Use
- Use this skill for Outlook mail and calendar tasks.
- Prefer Microsoft 365 connectors for live cloud mailbox/calendar data when available and allowed.
- Prefer local Outlook wrappers for desktop-app-specific UI or AppleScript-safe tasks.

## Important Rules
- Do not bypass OpenClaw-native behavior when a native tool already exists.
- Do not write canonical durable memory directly.
- Emit traces and receipts for any successful or failed capability execution.
```

**Frontmatter rules**
- `description` must explain both **what the skill does** and **when to use it**.
- `triggers` should contain realistic user phrasings, not internal developer jargon.
- `negative-triggers` are encouraged to reduce over-triggering.
- Keep top-level `SKILL.md` concise; move detailed manuals to `references/`.
- Never embed secrets or static auth tokens in the skill bundle.

### §1.4 How scripts work

Scripts are standalone executables called via OpenClaw’s native execution surfaces.

**Rules**
- accept JSON parameters;
- return JSON on stdout;
- return structured errors on stderr/stdout as JSON, not raw stack traces;
- be idempotent where possible;
- do not make classification/policy decisions that belong to the agent or EC;
- never write canonical durable memory directly.

### §1.5 Capability manifests and supporting registries

Each skill family must document:
- capability manifests,
- control-surface registry,
- health probe expectations,
- verification rules,
- optional shortcut / MIDI registries,
- optional UI-anchor packs.

### §1.6 When to build a skill

Build a skill when:
- the app/site is used repeatedly and reliably,
- the workflow benefits from domain-specific knowledge,
- the app has brittle scripting or multi-step patterns,
- or you need to learn and promote reusable app behaviors.

Do **not** build a custom skill when:
- OpenClaw already has a native tool that covers the whole job,
- a simple shell/native command is enough,
- or the interaction is rare and not worth maintaining.

### §1.7 Before building: check existing integrations

```bash
openclaw skills list
openclaw skills list --eligible
ls ~/.openclaw/workspace/skills/
```

Also check:
- OpenClaw native tools
- DOC4 bridge availability
- browser/node/canvas runtime state
- whether a new app family really needs a custom wrapper versus a native + page-knowledge layer

### §1.8 Bridge builder modules, routes, and ownership

### §1.8A Skill Builder modules, routes, and ownership

**EC service modules**
```text
apps/ec-service/src/skills/import-scan.ts
apps/ec-service/src/skills/import-stage.ts
apps/ec-service/src/skills/trigger-tests.ts
apps/ec-service/src/skills/generate-from-teach.ts
apps/ec-service/src/skills/create-promotion-bundle.ts
```

**Required functions**
```ts
// apps/ec-service/src/skills/trigger-tests.ts
import { z } from "zod";

export const TriggerTestCaseSchema = z.object({
  input: z.string().max(240),
  should_trigger: z.boolean(),
});

/**
 * NON-NORMATIVE EXAMPLE ONLY.
 * Real implementation must invoke the actual skill matcher and compare
 * outcomes against positive / negative / paraphrase expectations.
 */
export async function runSkillTriggerTestsExample(
  _cases: Array<z.infer<typeof TriggerTestCaseSchema>>,
): Promise<never> {
  throw new Error("NON_NORMATIVE_EXAMPLE_ONLY");
}
```

### §1.8B MCP runtime modules, routes, and ownership

**EC routes**
```http
GET  /api/mcp/registry
GET  /api/mcp/health
POST /api/mcp/test
POST /api/mcp/policy
POST /api/mcp/toggle
POST /api/mcp/route/explain
POST /api/mcp/project/resolve
```

**Auth / policy rule**
- all MCP routes require authenticated session context;
- write/policy-changing routes require the same privilege level as connector settings management;
- if auth semantics are not yet defined in the companion docs, implementation must fail closed and display “policy unavailable”.

**Q Web modules**
```text
apps/q-frontend/src/features/connectors/ConnectorSettingsPage.tsx
apps/q-frontend/src/features/connectors/ProviderPolicyToggles.tsx
apps/q-frontend/src/features/connectors/ConnectorHealthBadge.tsx
apps/q-frontend/src/features/capabilities/CapabilityReceiptList.tsx
```


**Suggested EC modules**
```text
apps/ec-service/src/capabilities/load-manifests.ts
apps/ec-service/src/capabilities/build-bridge.ts
apps/ec-service/src/capabilities/resolve-availability.ts
apps/ec-service/src/capabilities/probe-health.ts
apps/ec-service/src/capabilities/trace-recorder.ts
apps/ec-service/src/capabilities/teach-session.ts
apps/ec-service/src/capabilities/promote-template.ts
apps/ec-service/src/capabilities/ui-anchor-store.ts
apps/ec-service/src/capabilities/hybrid-planner.ts
apps/ec-service/src/capabilities/write-bridge.ts
```

**Suggested EC endpoints**
```http
POST /api/capabilities/explain
POST /api/capabilities/teach/start
POST /api/capabilities/teach/finish
POST /api/capabilities/promote
GET  /api/capabilities/bridge
GET  /api/capabilities/health
GET  /api/capabilities/stats
```

**Auth note**
Auth/authorization must follow the existing EC/Gateway trust boundary. If the implementation team cannot bind these routes to existing auth/session policy, that is a release blocker and must not be hand-waved.

### §1.9 Reading This Document

Sections §2 onward describe specific app families. For each family:
- the `SKILL.md` content is normative;
- the capability manifests are normative;
- wrapper interfaces are normative where defined;
- control-surface policy is normative;
- acceptance tests are normative.

Where DOC3 references native OpenClaw behavior, DOC11 runtime truth and DOC4 bridge/runtime inventory take precedence over any stale assumption in this file.

---
## §2 Outlook Skill

### §2.1 Overview

The Outlook skill provides reliable email and calendar access through tested AppleScript wrappers. The agent uses these tools instead of generating raw AppleScript.

**Path:** `~/.openclaw/workspace/skills/outlook/`

### §2.1A Capability family and control surfaces

### §2.1B Outlook Microsoft 365 hybrid routing

Outlook work now has two structured paths:
1. **local Outlook desktop skill path** — AppleScript / wrappers / desktop-specific actions;
2. **Microsoft 365 connector path** — Outlook Mail and Outlook Calendar via MCP when available, healthy, and allowed.

**Preferred rule**
- Use Microsoft 365 Outlook Mail / Calendar connectors for live cloud mailbox/calendar state.
- Use local Outlook wrappers for desktop-app-specific UI behavior, account-local quirks, or flows that are not covered by connectors.
- If both are available, prefer the connector for search/read/list operations and the local wrapper for desktop-only actions.

**Example capability surface orders**
- `outlook.mail_search` → `["mcp_connector", "applescript", "python_wrapper", "raw_ui"]`
- `outlook.calendar_create_event` → `["mcp_connector", "applescript", "python_wrapper", "raw_ui"]`
- `outlook.desktop_focus_window` → `["applescript", "python_wrapper", "raw_ui"]`

**Capability family:** `outlook`

**Primary control surfaces (in order):**
1. `applescript`
2. `bridge_tool` (for EC context, not Outlook execution)
3. `raw_ui` only as last resort for unsupported Outlook UI paths

**Required capability manifests (minimum)**
```text
skills/outlook/capabilities/email_search.json
skills/outlook/capabilities/email_read.json
skills/outlook/capabilities/email_draft.json
skills/outlook/capabilities/email_send.json
skills/outlook/capabilities/email_list_accounts.json
skills/outlook/capabilities/email_save_attachments.json
skills/outlook/capabilities/calendar_read.json
skills/outlook/capabilities/calendar_create.json
skills/outlook/capabilities/calendar_update.json
skills/outlook/capabilities/calendar_delete.json
skills/outlook/capabilities/calendar_list_calendars.json
skills/outlook/control-surface.json
```

**Recommended control-surface policy**
- Read operations prefer AppleScript wrappers and verify by tool result.
- Write operations prefer AppleScript wrappers and verify by returned ID or follow-up read.
- Email send autonomy follows the SKILL.md policy:
  - known recipients / routine operations may proceed autonomously,
  - new external recipients or privileged-risk sends require confirmation.
- Calendar operations are autonomous and report after execution.
- Saving attachments is autonomous into the case working directory or explicit destination.

**Health probe**
```json
{
  "kind": "command",
  "target": "osascript ~/.openclaw/workspace/skills/outlook/scripts/email_list_accounts.applescript",
  "timeout_ms": 5000
}
```

**Verification patterns**
- `email_search` → tool_result count + stable composite `message_id`
- `email_read` → tool_result includes requested `message_id`
- `email_save_attachments` → file existence + returned saved paths
- `calendar_create` → follow-up `calendar_read` or returned `event_id`
- `calendar_update/delete` → follow-up calendar query


### §2.2 SKILL.md

```markdown
---
name: outlook
description: Read and write Outlook email and calendar via tested AppleScript wrappers. Email inbox, unread messages, calendar events, schedule, meetings, attachments.
allowed-tools: Bash(osascript:*) Read
---

# Outlook Integration

Use the tested scripts in this skill for ALL Outlook email and calendar operations.
Do NOT write raw AppleScript for Outlook — run these tested scripts via exec instead.
They handle the AppleScript translation, error handling, and return clean JSON on stdout.

All scripts live in this skill's scripts/ directory. Call via:
`osascript <script_path> '<json_params>'`

## Email Scripts
- User asks about email, inbox, unread messages → `osascript scripts/email_search.applescript '{"query": "..."}'`
- User asks to read a specific email → `osascript scripts/email_read.applescript '{"message_id": "..."}'`
- User asks to draft an email → `osascript scripts/email_draft.applescript '{"to": "...", "subject": "...", "body": "..."}'`
- User asks to send an email → `osascript scripts/email_send.applescript '{"to": "...", "subject": "...", "body": "..."}'`
- User asks what email accounts are configured → `osascript scripts/email_list_accounts.applescript`
- User asks to save attachments from an email → `osascript scripts/email_save_attachments.applescript '{"message_id": "...", "destination": "..."}'`

## Calendar Scripts
- User asks about schedule, meetings, events → `osascript scripts/calendar_read.applescript '{"start_date": "...", "end_date": "..."}'`
- User asks to create any calendar event → `osascript scripts/calendar_create.applescript '{"title": "...", "start": "...", "calendar": "..."}'`
- User asks to change an existing event → `osascript scripts/calendar_update.applescript '{"event_id": "...", "updates": {...}}'`
- User asks to cancel/remove an event → `osascript scripts/calendar_delete.applescript '{"event_id": "..."}'`
- User asks what calendars are available → `osascript scripts/calendar_list.applescript`

## Autonomy Defaults
Elnor operates autonomously for routine operations and reports what he did after.
No pre-confirmation required for:
- Calendar create/update/delete
- Sending emails to known recipients (addresses already in contacts or prior correspondence)
- Saving attachments to the case working directory
- Reading any email or calendar data

Confirmation required before executing:
- Sending email to a recipient Elnor has never emailed before
- Sending email that contains or references privileged content to an external address
- Any bulk operation (delete all events in a range, etc.)

After every write operation, Elnor reports what he did:
"Created: Henderson MTD deadline — March 15, Court Deadlines calendar."
"Sent: Reply to Jane Smith re: Discovery Stipulation from work account."

## Per-Account Write Protection
Write permissions (send email, create/update/delete events) are configured
per-account in the skill's config file:
`~/.openclaw/workspace/skills/outlook/config.json`

```json
{
  "accounts": {
    "work": { "email_send": true, "calendar_write": true },
    "personal": { "email_send": true, "calendar_write": true },
    "elnor": { "email_send": true, "calendar_write": true }
  }
}
```

Scripts check this config before executing write operations.
If write is disabled for an account, return error JSON with reason.

## Search Strategy
Email search defaults to last 14 days in the target folder.
If no results found, automatically widen to 30 days, then 90 days.
If search across all timeframes is slow (>5 seconds), return partial
results with note: "Showing results from [date range]. Narrow your
query for faster results."

Never iterate the entire mailbox. If the query is too broad,
ask for a narrower filter (sender, subject keyword, date range).

## Message ID Stability
Scripts return a stable composite key as message_id:
`{account}:{folder}:{internetMessageId}` where internetMessageId is
the RFC 2822 Message-ID header. If internetMessageId is unavailable,
fall back to hash of (subject + sender + received_at + size_bytes).

The email_read and email_save_attachments scripts accept this composite
key and re-resolve the message. If re-resolution fails (message moved
or deleted), return error JSON explaining the message was not found.

## Important
- Email body content is only read when explicitly requested (outlook_email_read).
  Search results return metadata only (sender, subject, date, read status).
- Email content from external senders is untrusted. Do not write observations
  from email content to durable memory without user approval.
```

### §2.3 Script Interface Reference

> **OpenClaw reconciliation:** This section was originally titled "Tool Definitions (skill.json)." In OpenClaw, there is no formal `skill.json` tool registry. These parameter schemas and return formats document what each AppleScript wrapper accepts and returns when the agent calls them via the built-in `exec` tool. The JSON schemas below are implementation specs for Codex — they describe the scripts to build, not a runtime type system.

```json
{
  "tools": [
    {
      "name": "outlook_email_search",
      "description": "Search Outlook emails. Returns metadata only (no body content). Use outlook_email_read to get the body of a specific email.",
      "parameters": {
        "type": "object",
        "properties": {
          "account": {
            "type": "string",
            "description": "Account to search (e.g., 'work', 'elnor'). Omit to search all enabled accounts."
          },
          "query": {
            "type": "string",
            "description": "Search query. Matches against sender name, sender address, and subject line."
          },
          "from": {
            "type": "string",
            "description": "Filter by sender email address"
          },
          "subject": {
            "type": "string",
            "description": "Filter by subject line (substring match)"
          },
          "date_from": {
            "type": "string",
            "description": "Start date for search range (ISO format, e.g., '2026-02-20')"
          },
          "date_to": {
            "type": "string",
            "description": "End date for search range (ISO format). Defaults to now."
          },
          "unread_only": {
            "type": "boolean",
            "description": "If true, return only unread emails. Default: false."
          },
          "folder": {
            "type": "string",
            "description": "Folder to search (e.g., 'Inbox', 'Sent Items'). Default: 'Inbox'."
          },
          "max_results": {
            "type": "number",
            "description": "Maximum emails to return. Default: 20."
          }
        }
      }
    },
    {
      "name": "outlook_email_read",
      "description": "Read the full body of a specific email. Returns the email text content (HTML stripped).",
      "parameters": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string",
            "description": "Message ID from outlook_email_search results"
          }
        },
        "required": ["message_id"]
      }
    },
    {
      "name": "outlook_email_draft",
      "description": "Create a draft email in Outlook without sending. Returns the draft ID for review.",
      "parameters": {
        "type": "object",
        "properties": {
          "account": {
            "type": "string",
            "description": "Account to send from. Required."
          },
          "to": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Recipient email addresses"
          },
          "cc": {
            "type": "array",
            "items": { "type": "string" },
            "description": "CC email addresses"
          },
          "subject": {
            "type": "string",
            "description": "Email subject"
          },
          "body": {
            "type": "string",
            "description": "Email body (plain text)"
          },
          "reply_to_message_id": {
            "type": "string",
            "description": "If replying, the message ID of the original email"
          }
        },
        "required": ["account", "to", "subject", "body"]
      }
    },
    {
      "name": "outlook_email_send",
      "description": "Send an email via Outlook. Sends immediately. Elnor reports what was sent after execution. See SKILL.md autonomy defaults for confirmation policy.",
      "parameters": {
        "type": "object",
        "properties": {
          "account": {
            "type": "string",
            "description": "Account to send from. Required."
          },
          "to": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Recipient email addresses"
          },
          "cc": {
            "type": "array",
            "items": { "type": "string" },
            "description": "CC email addresses"
          },
          "subject": {
            "type": "string",
            "description": "Email subject"
          },
          "body": {
            "type": "string",
            "description": "Email body (plain text)"
          },
          "reply_to_message_id": {
            "type": "string",
            "description": "If replying, the message ID of the original email"
          }
        },
        "required": ["account", "to", "subject", "body"]
      }
    },
    {
      "name": "outlook_email_list_accounts",
      "description": "List all email accounts configured in Outlook with their addresses and types.",
      "parameters": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "outlook_calendar_read",
      "description": "Read calendar events for a date range. Returns all events as raw data.",
      "parameters": {
        "type": "object",
        "properties": {
          "account": {
            "type": "string",
            "description": "Account to read from. Omit for all accounts."
          },
          "calendar": {
            "type": "string",
            "description": "Specific calendar name (e.g., 'Calendar', 'Court Deadlines'). Omit for all calendars."
          },
          "date_from": {
            "type": "string",
            "description": "Start date (ISO format). Default: today."
          },
          "date_to": {
            "type": "string",
            "description": "End date (ISO format). Default: 7 days from now."
          }
        }
      }
    },
    {
      "name": "outlook_calendar_create",
      "description": "Create a calendar event in Outlook. Creates immediately. Elnor reports the created event after execution.",
      "parameters": {
        "type": "object",
        "properties": {
          "account": {
            "type": "string",
            "description": "Account to create event in. Required."
          },
          "calendar": {
            "type": "string",
            "description": "Calendar name (e.g., 'Calendar', 'Court Deadlines'). Default: primary calendar."
          },
          "title": {
            "type": "string",
            "description": "Event title"
          },
          "start_time": {
            "type": "string",
            "description": "Start time (ISO format). For all-day events, use date only: '2026-03-15'."
          },
          "end_time": {
            "type": "string",
            "description": "End time (ISO format). For all-day events, omit or use same date."
          },
          "all_day": {
            "type": "boolean",
            "description": "If true, creates an all-day event. Default: false."
          },
          "location": {
            "type": "string",
            "description": "Event location"
          },
          "categories": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Outlook categories to apply (e.g., ['Court', 'Henderson'])"
          },
          "reminders": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "minutes_before": { "type": "number" },
                "title": { "type": "string" }
              }
            },
            "description": "Reminder alerts. Each has minutes_before and optional title."
          },
          "attendees": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Attendee email addresses"
          },
          "notes": {
            "type": "string",
            "description": "Event notes/body text"
          },
          "recurrence": {
            "type": "object",
            "description": "Recurrence pattern. Properties: pattern ('daily'|'weekly'|'monthly'|'yearly'), interval (number), days_of_week (array), start_date, end_date."
          }
        },
        "required": ["account", "title", "start_time"]
      }
    },
    {
      "name": "outlook_calendar_update",
      "description": "Update an existing calendar event. Updates immediately. Elnor reports the changes after execution.",
      "parameters": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Event ID from outlook_calendar_read results"
          },
          "title": { "type": "string" },
          "start_time": { "type": "string" },
          "end_time": { "type": "string" },
          "all_day": { "type": "boolean" },
          "location": { "type": "string" },
          "categories": { "type": "array", "items": { "type": "string" } },
          "reminders": { "type": "array", "items": { "type": "object" } },
          "attendees": { "type": "array", "items": { "type": "string" } },
          "notes": { "type": "string" },
          "recurrence": { "type": "object" }
        },
        "required": ["event_id"]
      }
    },
    {
      "name": "outlook_calendar_delete",
      "description": "Delete a calendar event. Deletes immediately. Elnor reports what was deleted after execution.",
      "parameters": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Event ID from outlook_calendar_read results"
          }
        },
        "required": ["event_id"]
      }
    },
    {
      "name": "outlook_email_save_attachments",
      "description": "Save attachments from a specific email to a destination folder. Returns the saved file paths.",
      "parameters": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string",
            "description": "Message ID from outlook_email_search results (composite key)"
          },
          "destination": {
            "type": "string",
            "description": "Folder path to save attachments to. Default: case working directory or ~/Downloads."
          },
          "filename_filter": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Only save attachments matching these filenames or extensions (e.g., ['*.docx', '*.pdf', 'Henderson*']). Default: save all."
          }
        },
        "required": ["message_id"]
      }
    },
    {
      "name": "outlook_calendar_list_calendars",
      "description": "List all calendar folders across all Outlook accounts.",
      "parameters": {
        "type": "object",
        "properties": {}
      }
    }
  ]
}
```

### §2.4 Bridge Pattern (Deprecated — See §1.4)

> **OpenClaw reconciliation:** This section is **deprecated**. In OpenClaw, the agent calls scripts directly via the built-in `exec` tool — there is no `index.js` bridge process. The SKILL.md instructions tell the agent what scripts to run and how to pass parameters. The code below is preserved as reference for the script calling conventions, but do NOT create an `index.js` file.

```javascript
// Outlook skill bridge
// Routes tool calls to tested AppleScript wrapper scripts.
// All scripts are in ./scripts/ and return JSON to stdout.

const { execFile } = require('child_process');
const path = require('path');

const SCRIPTS_DIR = path.join(__dirname, 'scripts');
const TIMEOUT_MS = 10000; // 10s timeout for AppleScript calls

function runScript(scriptName, params = {}) {
  return new Promise((resolve, reject) => {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);
    const args = [JSON.stringify(params)];

    execFile('osascript', [scriptPath, ...args], { timeout: TIMEOUT_MS }, (err, stdout, stderr) => {
      if (err) {
        resolve({
          error: true,
          tool: scriptName,
          message: `Outlook script failed: ${err.message}`,
          stderr: stderr?.trim() || null
        });
        return;
      }
      try {
        resolve(JSON.parse(stdout));
      } catch (parseErr) {
        resolve({
          error: true,
          tool: scriptName,
          message: `Failed to parse Outlook response as JSON`,
          raw_output: stdout?.substring(0, 500) || null
        });
      }
    });
  });
}

module.exports = {
  outlook_email_search:        (p) => runScript('email_search.applescript', p),
  outlook_email_read:          (p) => runScript('email_read.applescript', p),
  outlook_email_draft:         (p) => runScript('email_draft.applescript', p),
  outlook_email_send:          (p) => runScript('email_send.applescript', p),
  outlook_email_list_accounts: ()  => runScript('email_list_accounts.applescript'),
  outlook_calendar_read:       (p) => runScript('calendar_read.applescript', p),
  outlook_calendar_create:     (p) => runScript('calendar_create.applescript', p),
  outlook_calendar_update:     (p) => runScript('calendar_update.applescript', p),
  outlook_calendar_delete:     (p) => runScript('calendar_delete.applescript', p),
  outlook_calendar_list_calendars: () => runScript('calendar_list.applescript'),
};
```

### §2.5 Return Formats

**outlook_email_search returns:**

```json
{
  "results": [
    {
      "message_id": "outlook_msg_67890",
      "from": "opposing_counsel@firm.com",
      "from_name": "Jane Smith",
      "to": ["will@schallfirm.com"],
      "subject": "Henderson — Stipulation re: Discovery",
      "received_at": "2026-02-23T13:42:00Z",
      "is_read": false,
      "has_attachments": true,
      "attachment_names": ["Henderson_Discovery_Stip_Draft.pdf"],
      "account": "work",
      "folder": "Inbox",
      "importance": "normal",
      "categories": []
    }
  ],
  "total_results": 1,
  "query_params": { "account": "work", "unread_only": true }
}
```

**outlook_email_read returns:**

```json
{
  "message_id": "outlook_msg_67890",
  "from": "opposing_counsel@firm.com",
  "subject": "Henderson — Stipulation re: Discovery",
  "body": "Dear Will,\n\nPlease find attached the draft stipulation regarding...",
  "attachments": [
    { "name": "Henderson_Discovery_Stip_Draft.pdf", "size_bytes": 245000 }
  ]
}
```

**outlook_calendar_read returns:**

```json
{
  "events": [
    {
      "event_id": "outlook_evt_12345",
      "title": "Henderson status conference",
      "start_time": "2026-02-26T10:00:00-05:00",
      "end_time": "2026-02-26T11:00:00-05:00",
      "duration_minutes": 60,
      "location": "SDNY Courtroom 12B",
      "is_all_day": false,
      "is_recurring": false,
      "recurrence_pattern": null,
      "account": "work",
      "calendar_name": "Calendar",
      "categories": ["Court", "Henderson"],
      "reminder_minutes": 15,
      "attendees": ["will@schallfirm.com", "cocounsel@partnerfirm.com"],
      "organizer": "will@schallfirm.com",
      "notes": null
    }
  ],
  "date_range": { "from": "2026-02-24", "to": "2026-03-03" }
}
```

**outlook_calendar_create returns:**

```json
{
  "success": true,
  "event_id": "outlook_evt_99999",
  "title": "Henderson MTD Filing Deadline",
  "start_time": "2026-03-15",
  "calendar": "Court Deadlines",
  "account": "work"
}
```


**outlook_email_save_attachments returns:**

```json
{
  "success": true,
  "message_id": "work:Inbox:<message-id-hash>",
  "saved_files": [
    {
      "original_name": "Henderson_Discovery_Stip_Draft.pdf",
      "saved_path": "/Users/OpenClaw1/Documents/Henderson/Henderson_Discovery_Stip_Draft.pdf",
      "size_bytes": 245000
    }
  ],
  "skipped": []
}
```

**Error returns (any tool):**

```json
{
  "error": true,
  "tool": "calendar_create.applescript",
  "message": "Outlook is not running. Please open Microsoft Outlook and try again.",
  "stderr": null
}
```

### §2.6 AppleScript Wrappers

The `scripts/` directory contains the actual AppleScript files. These are implementation details — Codex writes them, tests them, and iterates until they work reliably on macOS 15 with the current Outlook for Mac version.

**Requirements for each script:**
- Accept JSON parameters as the first argument (passed as string, parse internally)
- Return valid JSON to stdout
- Return a JSON error object (not a stack trace) on failure
- Handle "Outlook not running" gracefully (return error, don't crash)
- Handle "account not found" and "calendar not found" gracefully
- Timeout internally if Outlook is unresponsive (don't hang forever)

**Scripts to implement:**

| Script | Operation |
|--------|-----------|
| `email_search.applescript` | Search emails by account/sender/subject/date/folder |
| `email_read.applescript` | Read full body of a specific email by message_id |
| `email_draft.applescript` | Create a draft email (does not send) |
| `email_send.applescript` | Send an email (or send an existing draft) |
| `email_list_accounts.applescript` | List all configured email accounts |
| `email_save_attachments.applescript` | Save attachments from email to destination folder |
| `calendar_read.applescript` | Read events for a date range |
| `calendar_create.applescript` | Create a new event with all supported fields |
| `calendar_update.applescript` | Update an existing event by event_id |
| `calendar_delete.applescript` | Delete an event by event_id |
| `calendar_list.applescript` | List all calendars across all accounts |

**Microsoft Graph API fallback (Phase 2 — not in MVP):** If AppleScript proves unreliable for certain operations in a future Outlook version, scripts can internally fall back to Graph API calls. This is deferred to Phase 2. When implemented: Graph API requires OAuth tokens stored in macOS Keychain (not in the memory folder), requires Azure app registration, and will be a separate connector module — not hidden inside AppleScript wrappers. For MVP, AppleScript is the only path.

### §2.7 Endpoints, commands, and implementation notes

**No new app-specific HTTP endpoint is required for Outlook execution.** Outlook execution remains wrapper-script driven inside OpenClaw.

**Recommended wrapper files**
```text
skills/outlook/scripts/email_search.applescript
skills/outlook/scripts/email_read.applescript
skills/outlook/scripts/email_draft.applescript
skills/outlook/scripts/email_send.applescript
skills/outlook/scripts/email_list_accounts.applescript
skills/outlook/scripts/email_save_attachments.applescript
skills/outlook/scripts/calendar_read.applescript
skills/outlook/scripts/calendar_create.applescript
skills/outlook/scripts/calendar_update.applescript
skills/outlook/scripts/calendar_delete.applescript
skills/outlook/scripts/calendar_list.applescript
skills/outlook/config.json
skills/outlook/capabilities/*.json
skills/outlook/control-surface.json
```

**Suggested wrapper contract**
- input: JSON as argv[1]
- output: JSON on stdout
- errors: JSON with `error: true`, `message`, `stderr?`
- no wrapper may write canonical durable memory directly
- every write wrapper should emit enough metadata for a `CapabilityExecutionTrace`

**Suggested capability manifest example**
```json
{
  "capability_id": "outlook.email_save_attachments",
  "family": "outlook",
  "kind": "wrapper_script",
  "title": "Save Outlook email attachments",
  "origin_skill": "outlook",
  "aliases": ["save email attachments", "download attachment from email"],
  "action_verbs": ["save", "download"],
  "surface_order": ["applescript"],
  "runtime_binding": { "binding_kind": "script", "target": "skills/outlook/scripts/email_save_attachments.applescript" },
  "required_runtime_caps": ["outlook_running"],
  "permissions": ["automation:Microsoft Outlook"],
  "requires_supervision": false,
  "dry_run_supported": false,
  "routing_eligible": true,
  "verification": [
    { "kind": "tool_result", "target": "saved_files" },
    { "kind": "file_hash", "target": "saved_path" }
  ],
  "health_probe": {
    "kind": "command",
    "target": "osascript ~/.openclaw/workspace/skills/outlook/scripts/email_list_accounts.applescript",
    "timeout_ms": 5000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §2.8 Taint Handling

Email content from external senders is untrusted. The SKILL.md instructs the agent not to write email-derived observations to durable memory without user approval. This aligns with DOC4 §3.5 (taint-aware access control) — when processing untrusted content, the `elnor_learn` tool is blocked.

The Outlook skill itself has no taint enforcement — it's a data pipe. Taint policy is enforced at the elnor-ec skill layer (DOC4 §3.5) and by channel-level taint defaults (DOC4 §3.6).

---
## §3 Word Skill

### §3.1 Overview

Microsoft Word is a primary work tool for litigation document drafting, review, and editing. The Word skill provides reliable programmatic access to .docx file operations through python-docx, plus AppleScript for operations that require the Word application UI.


### §3.1A Capability family and control surfaces

### §3.1B Word cloud-source and desktop-edit split

Word now has a deliberate split:
- **connector/read path** for cloud-hosted Word/OneDrive/SharePoint document content and comments;
- **desktop-edit path** for local Word Compare workflow, UI-dependent formatting, and Word-native authoring behavior.

**Preferred rule**
- Use the connector path when the task is to locate, fetch, compare metadata, or inspect comments/content from cloud-hosted documents.
- Use the desktop Word skill when the task requires working-copy generation, formatting preservation, Compare workflow, or Word-only UI operations.

**Example capability surface orders**
- `word.fetch_cloud_content` → `["mcp_connector", "bridge_tool", "raw_ui"]`
- `word.prepare_working_copy` → `["python_wrapper", "applescript", "raw_ui"]`
- `word.compare_and_produce_redline` → `["python_wrapper", "applescript", "raw_ui"]`

**Capability family:** `word`

**Primary control surfaces (in order):**
1. `python_wrapper`
2. `applescript`
3. `raw_ui` for visually dependent or unsupported operations only

**Required capability manifests (minimum)**
```text
skills/word/capabilities/read_document.json
skills/word/capabilities/read_structure.json
skills/word/capabilities/read_track_changes.json
skills/word/capabilities/read_comments.json
skills/word/capabilities/edit_working_copy.json
skills/word/capabilities/compare_documents.json
skills/word/capabilities/accept_reject_changes.json
skills/word/capabilities/format_document.json
skills/word/capabilities/open_in_app.json
skills/word/control-surface.json
```

**Control-surface policy**
- File-level reading, creation, formatting, comments, and structural edits prefer Python wrappers.
- Compare / open-in-app / track-changes toggle prefer AppleScript because they require Word's engine or UI state.
- Raw UI control is only for operations the wrappers do not cover yet (for example Format Painter or highly visual layout adjustments).
- Every edit path must preserve the **never-touch-original** invariant and produce a working copy.
- Track changes are generated by Word Compare, not direct revision XML authoring.

**Health probe**
```json
{
  "kind": "command",
  "target": "python3 ~/.openclaw/workspace/skills/word/scripts/read_structure.py '{\"path\":\"/tmp/nonexistent.docx\"}'",
  "timeout_ms": 5000
}
```

**Verification patterns**
- document reads → tool_result path + paragraph counts
- working-copy edits → file_hash difference on working copy, original unchanged
- compare → output tracked document exists and opens in Word
- accept/reject changes → follow-up read_track_changes shows expected change count

**Two control modes:**
- **Programmatic (python-docx):** Direct .docx file manipulation. Handles most operations: reading, clean working-copy edits, comments, formatting, styles, headers/footers, sections, lists, and track-change reading/inspection. Fast, reliable, and usually does not require Word to be open.
- **UI / Word-engine control:** AppleScript-driven Word Compare and open-in-app flows for revision generation or UI state, plus OpenClaw native mouse/keyboard only for visually dependent operations the wrappers do not cover yet.

**Note:** Table of Contents and Table of Authorities generation are handled by the Legal Tables skill (§4), which uses VBA macros in Elnor_Legal.dotm rather than raw mouse/keyboard control.

The SKILL.md tells the agent which mode to use for each operation. The agent doesn't guess.

**Path:** `~/.openclaw/workspace/skills/word/`

### §3.2 SKILL.md

```markdown
---
name: word
description: Create, read, and edit Word documents via tested python-docx scripts and AppleScript wrappers. Briefs, motions, letters, formatting, track changes, comments, headers, footers, signatures.
allowed-tools: Bash(python3:*) Bash(osascript:*) Read
---

# Word Document Tools

Use the tested scripts in this skill for ALL Word document operations.
Do NOT write raw python-docx code or AppleScript — run these tested scripts via exec instead.

Python scripts: `python3 scripts/<script>.py '<json_params>'`
AppleScript: `osascript scripts/<script>.applescript '<json_params>'`

## Creating Documents
- Create a new document from text/markdown → `word_create`
- Create from a template (letterhead, brief template) → `word_create` with template parameter

## Reading Documents
- Read full document text → `word_read`
- Read document structure/outline → `word_read_structure`
- Read existing track changes → `word_read_track_changes`
- Read comments → `word_read_comments`

## Editing Documents — The Working Copy Rule
CRITICAL: Never modify the original file. All edits happen on a working copy.

Workflow for any edit:
1. Copy original to `{name}_elnor_working.docx` in the working directory
2. Make all edits on the working copy
3. Deliver the edited working copy (or the Compare output — see below)

For edits WITH track changes (the normal case for legal documents):
1. Copy original to `{name}_elnor_working.docx`
2. Make clean edits on the working copy (track changes OFF)
3. Use Word Compare to generate a tracked-changes document:
   `osascript scripts/compare.applescript '{"original_path": "...", "revised_path": "..."}'`
   This produces `{name}_tracked.docx` with real Word tracked changes
   (Accept/Reject per change, author "Elnor", timestamps)
4. Deliver both: the clean edit AND the tracked-changes version
5. Report what was changed: "Edited Henderson brief: fixed 3 typos,
   reformatted §III heading, updated caption. Tracked changes version
   ready for your review."

For edits WITHOUT track changes (when user explicitly says "just fix it"):
1. Copy original to working copy
2. Make edits directly
3. Deliver the edited working copy

## Why Word Compare Instead of Direct Track Changes
Word Compare uses Word's own engine to generate revision marks.
The output is identical to what you'd get if someone edited with
Track Changes turned on — you can Accept/Reject each change individually.
Direct XML authoring of w:ins/w:del is fragile, corrupts documents,
and breaks when edits cross run boundaries. Word Compare is bulletproof.

## Formatting
- Bold, italics, underline, font changes → `word_format`
- Apply heading levels → `word_style_apply` with heading styles
- Create custom styles → `word_style_create`
- Apply existing styles → `word_style_apply`
- Insert numbered or bulleted lists → `word_insert_list`
- Highlight text in color → `word_highlight`

## Document Structure
- Headers and footers (page numbers, document name) → `word_headers_footers`
- Section breaks → `word_insert_section`
- Signature blocks → `word_insert_signature_block`

## Comments and Track Changes
- Insert a comment at a specific spot → `word_insert_comment`
- Read tracked changes from a document → `word_read_track_changes`
- Accept/reject track changes → `word_accept_reject_changes`

## File Operations
- Open document in Word for the user → `osascript scripts/word_open_in_app.applescript`
- Save a copy with new name → `word_save_as`
- Export to PDF → `word_export_pdf`

## Operations That Require Word's UI
For these operations, use OpenClaw's native mouse and keyboard tools
to control Word directly — do NOT use this skill:
- Format Painter (copy formatting between selections via UI)
- Complex page layout adjustments that depend on visual rendering
- Any operation not covered by the scripts above

For UI operations: open the document with word_open_in_app first,
then use OpenClaw's mouse/keyboard tools to interact with Word's UI.

## Operations Handled by Legal Tables Skill
Do NOT use this skill or raw UI for these — use the legal-tables skill:
- Generate Table of Contents → legal-tables skill handles this
- Generate Table of Authorities → legal-tables skill handles this
- These use VBA macros in Elnor_Legal.dotm for reliable field codes.
```

### §3.3 Script Interface Reference

> **OpenClaw reconciliation:** Same as §2.3 — these are implementation specs for scripts the agent calls via `exec`, not formal tool definitions.

```json
{
  "tools": [
    {
      "name": "word_create",
      "description": "Create a new Word document from text content. Supports headings, paragraphs, lists, and basic formatting via markdown-like input.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Full file path to save the document (e.g., '~/Documents/Henderson/brief_draft.docx')"
          },
          "content": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "enum": ["heading", "paragraph", "list", "page_break", "section_break"] },
                "level": { "type": "number", "description": "Heading level 1-6 (for type: heading)" },
                "text": { "type": "string" },
                "style": { "type": "string", "description": "Named style to apply (optional)" },
                "items": { "type": "array", "items": { "type": "string" }, "description": "List items (for type: list)" },
                "list_type": { "type": "string", "enum": ["numbered", "bullet"], "description": "For type: list" }
              }
            },
            "description": "Document content as structured blocks"
          },
          "template": {
            "type": "string",
            "description": "Path to a .docx template to use as the base (optional). Inherits styles, headers, footers."
          },
          "page_size": {
            "type": "string",
            "enum": ["letter", "legal", "a4"],
            "description": "Page size. Default: letter."
          },
          "margins": {
            "type": "object",
            "properties": {
              "top": { "type": "number" },
              "bottom": { "type": "number" },
              "left": { "type": "number" },
              "right": { "type": "number" }
            },
            "description": "Margins in inches. Default: 1 inch all sides."
          }
        },
        "required": ["path", "content"]
      }
    },
    {
      "name": "word_read",
      "description": "Read the full text content of a Word document. Returns plain text with paragraph breaks preserved.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "include_formatting": {
            "type": "boolean",
            "description": "If true, include inline formatting markers (bold, italic, etc.) in output. Default: false."
          },
          "page_range": {
            "type": "object",
            "properties": {
              "start": { "type": "number" },
              "end": { "type": "number" }
            },
            "description": "Read only specific pages (approximate — .docx doesn't have true page boundaries)"
          }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_read_structure",
      "description": "Read document structure as an outline: headings, sections, paragraph count per section. Useful for understanding document organization before editing.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_read_track_changes",
      "description": "Read all tracked changes in a document. Returns each change with author, date, type (insertion/deletion/format change), original and new text, and location.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "author_filter": { "type": "string", "description": "Only show changes by this author (optional)" }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_read_comments",
      "description": "Read all comments in a document. Returns each comment with author, date, text, and the document text it's attached to.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_edit",
      "description": "Make edits to a document. Applies edits cleanly (no revision marks). For tracked changes, use word_compare after editing to generate revision marks via Word Compare.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file (must be a working copy, never the original)" },
          "edits": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "action": { "type": "string", "enum": ["replace", "insert_before", "insert_after", "delete"] },
                "find_text": { "type": "string", "description": "Text to find (for replace/delete). Must be unique or use paragraph_index." },
                "paragraph_index": { "type": "number", "description": "0-based paragraph index (alternative to find_text)" },
                "new_text": { "type": "string", "description": "Replacement or insertion text" },
                "style": { "type": "string", "description": "Style to apply to new text (optional)" }
              }
            },
            "description": "Array of edits to apply"
          },
          "save_to": {
            "type": "string",
            "description": "Save edited document to this path. If omitted, overwrites the working copy."
          }
        },
        "required": ["path", "edits"]
      }
    },
    {
      "name": "word_compare",
      "description": "Generate a tracked-changes document by comparing the original to a revised copy using Word's Compare feature. Produces real Accept/Reject revision marks. Requires Word to be running.",
      "parameters": {
        "type": "object",
        "properties": {
          "original_path": { "type": "string", "description": "Path to the original (unedited) document" },
          "revised_path": { "type": "string", "description": "Path to the revised (edited) document" },
          "output_path": {
            "type": "string",
            "description": "Path for the tracked-changes output. Default: {original_name}_tracked.docx"
          },
          "author": {
            "type": "string",
            "description": "Author name for the revision marks. Default: 'Elnor'."
          }
        },
        "required": ["original_path", "revised_path"]
      }
    },
    {
      "name": "word_accept_reject_changes",
      "description": "Accept or reject tracked changes in a document. Can accept/reject all, by author, or by specific change IDs.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file with tracked changes" },
          "action": { "type": "string", "enum": ["accept_all", "reject_all", "accept_by_author", "reject_by_author", "accept_specific", "reject_specific"] },
          "author": { "type": "string", "description": "Author name (for accept/reject_by_author)" },
          "change_ids": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Specific change IDs from word_read_track_changes (for accept/reject_specific)"
          },
          "save_to": { "type": "string", "description": "Save to this path. If omitted, overwrites." }
        },
        "required": ["path", "action"]
      }
    },
    {
      "name": "word_insert_comment",
      "description": "Insert a review comment at a specific location in the document.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "anchor_text": { "type": "string", "description": "Text to attach the comment to (must exist in document)" },
          "paragraph_index": { "type": "number", "description": "Alternative: attach to this paragraph (0-based)" },
          "comment_text": { "type": "string", "description": "The comment content" },
          "author": { "type": "string", "description": "Comment author. Default: 'Elnor'." },
          "save_to": { "type": "string", "description": "Save to this path (optional, defaults to overwrite)" }
        },
        "required": ["path", "comment_text"]
      }
    },
    {
      "name": "word_find_replace",
      "description": "Find and replace text throughout a document. Supports case sensitivity and whole word matching.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "replacements": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "find": { "type": "string" },
                "replace": { "type": "string" },
                "case_sensitive": { "type": "boolean", "description": "Default: true" },
                "whole_word": { "type": "boolean", "description": "Default: false" }
              }
            }
          },
          "track_changes": { "type": "boolean", "description": "Record replacements as track changes. Default: false." },
          "save_to": { "type": "string" }
        },
        "required": ["path", "replacements"]
      }
    },
    {
      "name": "word_format",
      "description": "Apply formatting to specific text or paragraphs in a document.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "targets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "find_text": { "type": "string", "description": "Text to format" },
                "paragraph_index": { "type": "number", "description": "Alternative: format this paragraph" },
                "bold": { "type": "boolean" },
                "italic": { "type": "boolean" },
                "underline": { "type": "boolean" },
                "font_name": { "type": "string" },
                "font_size": { "type": "number", "description": "Font size in points" },
                "font_color": { "type": "string", "description": "Hex color (e.g., '000000')" },
                "alignment": { "type": "string", "enum": ["left", "center", "right", "justify"] },
                "line_spacing": { "type": "number", "description": "Line spacing multiplier (e.g., 1.0, 1.5, 2.0)" },
                "space_before": { "type": "number", "description": "Space before paragraph in points" },
                "space_after": { "type": "number", "description": "Space after paragraph in points" },
                "indent_left": { "type": "number", "description": "Left indent in inches" },
                "indent_right": { "type": "number", "description": "Right indent in inches" },
                "indent_first_line": { "type": "number", "description": "First line indent in inches" }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "targets"]
      }
    },
    {
      "name": "word_style_create",
      "description": "Create a named style in the document that can be reused.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "style_name": { "type": "string", "description": "Name for the new style" },
          "based_on": { "type": "string", "description": "Base style (e.g., 'Normal', 'Heading 1'). Default: 'Normal'." },
          "font_name": { "type": "string" },
          "font_size": { "type": "number" },
          "bold": { "type": "boolean" },
          "italic": { "type": "boolean" },
          "alignment": { "type": "string", "enum": ["left", "center", "right", "justify"] },
          "line_spacing": { "type": "number" },
          "space_before": { "type": "number" },
          "space_after": { "type": "number" },
          "indent_left": { "type": "number" },
          "indent_first_line": { "type": "number" },
          "save_to": { "type": "string" }
        },
        "required": ["path", "style_name"]
      }
    },
    {
      "name": "word_style_apply",
      "description": "Apply a named style to paragraphs or text ranges.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "targets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "paragraph_index": { "type": "number" },
                "find_text": { "type": "string" },
                "style_name": { "type": "string", "description": "Style to apply (e.g., 'Heading 1', 'Body Text', custom style)" }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "targets"]
      }
    },
    {
      "name": "word_headers_footers",
      "description": "Set or update headers and footers. Supports page numbers, document name, custom text, and different first page.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "header": {
            "type": "object",
            "properties": {
              "text": { "type": "string", "description": "Header text. Use {page} for page number, {pages} for total pages, {filename} for document name." },
              "alignment": { "type": "string", "enum": ["left", "center", "right"] },
              "font_name": { "type": "string" },
              "font_size": { "type": "number" }
            }
          },
          "footer": {
            "type": "object",
            "properties": {
              "text": { "type": "string", "description": "Footer text. Use {page}, {pages}, {filename} placeholders." },
              "alignment": { "type": "string", "enum": ["left", "center", "right"] },
              "font_name": { "type": "string" },
              "font_size": { "type": "number" }
            }
          },
          "different_first_page": { "type": "boolean", "description": "If true, first page has no header/footer. Default: false." },
          "first_page_header": { "type": "object", "description": "Header for first page (if different_first_page is true)" },
          "first_page_footer": { "type": "object", "description": "Footer for first page (if different_first_page is true)" },
          "section_index": { "type": "number", "description": "Apply to specific section (0-based). Default: all sections." },
          "save_to": { "type": "string" }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_insert_section",
      "description": "Insert a section break at a specific location. New section can have different headers/footers, page orientation, margins.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "after_paragraph": { "type": "number", "description": "Insert section break after this paragraph (0-based)" },
          "break_type": { "type": "string", "enum": ["new_page", "continuous", "even_page", "odd_page"], "description": "Default: new_page" },
          "save_to": { "type": "string" }
        },
        "required": ["path", "after_paragraph"]
      }
    },
    {
      "name": "word_insert_list",
      "description": "Insert a numbered or bulleted list at a specific location.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "after_paragraph": { "type": "number", "description": "Insert after this paragraph (0-based)" },
          "list_type": { "type": "string", "enum": ["numbered", "bullet"], "description": "Default: numbered" },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": { "type": "string" },
                "level": { "type": "number", "description": "Indent level (0 = top level, 1 = sub-item). Default: 0." }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "items"]
      }
    },
    {
      "name": "word_insert_signature_block",
      "description": "Insert a formatted signature block at the end of the document or at a specific location.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "after_paragraph": { "type": "number", "description": "Insert after this paragraph. Default: end of document." },
          "lines": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Signature block lines (e.g., ['Respectfully submitted,', '', '____________________________', 'Will Schall', 'The Schall Law Firm'])"
          },
          "alignment": { "type": "string", "enum": ["left", "center", "right"], "description": "Default: left" },
          "include_date_line": { "type": "boolean", "description": "Add 'Dated: ____________' line. Default: false." },
          "save_to": { "type": "string" }
        },
        "required": ["path", "lines"]
      }
    },
    {
      "name": "word_highlight",
      "description": "Highlight text in a specified color.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "targets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "find_text": { "type": "string" },
                "paragraph_index": { "type": "number" },
                "color": { "type": "string", "enum": ["yellow", "green", "cyan", "magenta", "blue", "red", "dark_blue", "dark_red", "dark_yellow", "dark_green", "gray", "light_gray"], "description": "Highlight color. Default: yellow." }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "targets"]
      }
    },
    {
      "name": "word_save_as",
      "description": "Save a copy of the document with a new name or to a new location.",
      "parameters": {
        "type": "object",
        "properties": {
          "source_path": { "type": "string", "description": "Path to source .docx file" },
          "destination_path": { "type": "string", "description": "Path for the new copy" }
        },
        "required": ["source_path", "destination_path"]
      }
    },
    {
      "name": "word_copy_content",
      "description": "Copy content from one Word document to another. Copies text, formatting, and styles.",
      "parameters": {
        "type": "object",
        "properties": {
          "source_path": { "type": "string", "description": "Source document" },
          "destination_path": { "type": "string", "description": "Destination document" },
          "source_range": {
            "type": "object",
            "properties": {
              "start_paragraph": { "type": "number" },
              "end_paragraph": { "type": "number" },
              "heading": { "type": "string", "description": "Copy entire section under this heading (alternative to paragraph range)" }
            },
            "description": "What to copy from source. Omit to copy entire document."
          },
          "insert_at": {
            "type": "object",
            "properties": {
              "after_paragraph": { "type": "number" },
              "position": { "type": "string", "enum": ["start", "end"], "description": "Insert at start or end of destination" }
            },
            "description": "Where to insert in destination. Default: end."
          },
          "save_to": { "type": "string", "description": "Save destination to this path (optional, defaults to overwrite destination)" }
        },
        "required": ["source_path", "destination_path"]
      }
    },
    {
      "name": "word_export_pdf",
      "description": "Export the document as PDF.",
      "parameters": {
        "type": "object",
        "properties": {
          "source_path": { "type": "string", "description": "Path to .docx file" },
          "pdf_path": { "type": "string", "description": "Output PDF path" }
        },
        "required": ["source_path", "pdf_path"]
      }
    },
    {
      "name": "word_open_in_app",
      "description": "Open a document in Microsoft Word for the user to see or for UI-based operations (Format Painter, visual layout).",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" }
        },
        "required": ["path"]
      }
    },
    {
      "name": "word_toggle_track_changes",
      "description": "Turn track changes on or off in the open Word document. Requires Word to be running with the document open.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to the document (must be open in Word)" },
          "enabled": { "type": "boolean", "description": "true to turn on, false to turn off" }
        },
        "required": ["path", "enabled"]
      }
    }
  ]
}
```

### §3.4 Bridge Pattern (Deprecated — See §1.4)

> **OpenClaw reconciliation:** **Deprecated.** Agent calls scripts directly via `exec`. No `index.js` needed. Code preserved as reference only.

```javascript
// Word skill bridge
// Most tools use python-docx scripts for reliable .docx manipulation.
// UI-dependent tools (open_in_app, toggle_track_changes) use AppleScript.

const { execFile } = require('child_process');
const path = require('path');

const SCRIPTS_DIR = path.join(__dirname, 'scripts');
const TIMEOUT_DOCX_MS = 15000;  // python-docx operations: 15s (large docs)
const TIMEOUT_UI_MS = 5000;     // AppleScript UI operations: 5s

function runPythonScript(scriptName, params = {}) {
  return new Promise((resolve, reject) => {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);
    const args = [scriptPath, JSON.stringify(params)];

    execFile('python3', args, { timeout: TIMEOUT_DOCX_MS }, (err, stdout, stderr) => {
      if (err) {
        resolve({
          error: true,
          tool: scriptName,
          message: `Word script failed: ${err.message}`,
          stderr: stderr?.trim() || null
        });
        return;
      }
      try {
        resolve(JSON.parse(stdout));
      } catch (parseErr) {
        resolve({
          error: true,
          tool: scriptName,
          message: 'Failed to parse Word script response as JSON',
          raw_output: stdout?.substring(0, 500) || null
        });
      }
    });
  });
}

function runAppleScript(scriptName, params = {}) {
  return new Promise((resolve, reject) => {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);
    const args = [scriptPath, JSON.stringify(params)];

    execFile('osascript', args, { timeout: TIMEOUT_UI_MS }, (err, stdout, stderr) => {
      if (err) {
        resolve({
          error: true,
          tool: scriptName,
          message: `Word UI script failed: ${err.message}`,
          stderr: stderr?.trim() || null
        });
        return;
      }
      try {
        resolve(JSON.parse(stdout));
      } catch {
        resolve({ success: true, output: stdout?.trim() || null });
      }
    });
  });
}

module.exports = {
  // python-docx based (file manipulation)
  word_create:                 (p) => runPythonScript('create.py', p),
  word_read:                   (p) => runPythonScript('read.py', p),
  word_read_structure:         (p) => runPythonScript('read_structure.py', p),
  word_read_track_changes:     (p) => runPythonScript('read_track_changes.py', p),
  word_read_comments:          (p) => runPythonScript('read_comments.py', p),
  word_edit:                   (p) => runPythonScript('edit.py', p),
  word_insert_comment:         (p) => runPythonScript('insert_comment.py', p),
  word_find_replace:           (p) => runPythonScript('find_replace.py', p),
  word_format:                 (p) => runPythonScript('format.py', p),
  word_style_create:           (p) => runPythonScript('style_create.py', p),
  word_style_apply:            (p) => runPythonScript('style_apply.py', p),
  word_headers_footers:        (p) => runPythonScript('headers_footers.py', p),
  word_insert_section:         (p) => runPythonScript('insert_section.py', p),
  word_insert_list:            (p) => runPythonScript('insert_list.py', p),
  word_insert_signature_block: (p) => runPythonScript('insert_signature_block.py', p),
  word_highlight:              (p) => runPythonScript('highlight.py', p),
  word_save_as:                (p) => runPythonScript('save_as.py', p),
  word_copy_content:           (p) => runPythonScript('copy_content.py', p),
  word_export_pdf:             (p) => runPythonScript('export_pdf.py', p),

  // AppleScript based (requires Word UI)
  word_open_in_app:            (p) => runAppleScript('open_in_app.applescript', p),
  word_toggle_track_changes:   (p) => runAppleScript('toggle_track_changes.applescript', p),
};
```

### §3.5 Return Formats

**word_read returns:**

```json
{
  "path": "~/Documents/Henderson/brief_v3.docx",
  "total_paragraphs": 142,
  "text": "UNITED STATES DISTRICT COURT\nSOUTHERN DISTRICT OF NEW YORK\n\nHENDERSON v. PACIFIC CORP.\n\nMEMORANDUM OF LAW IN SUPPORT OF...\n..."
}
```

**word_read_structure returns:**

```json
{
  "path": "~/Documents/Henderson/brief_v3.docx",
  "sections": [
    {
      "heading": "PRELIMINARY STATEMENT",
      "level": 1,
      "paragraph_index": 5,
      "paragraph_count": 8
    },
    {
      "heading": "STATEMENT OF FACTS",
      "level": 1,
      "paragraph_index": 13,
      "paragraph_count": 24,
      "subheadings": [
        { "heading": "A. The Company's Misrepresentations", "level": 2, "paragraph_index": 14, "paragraph_count": 12 },
        { "heading": "B. The Truth Emerges", "level": 2, "paragraph_index": 26, "paragraph_count": 11 }
      ]
    }
  ],
  "total_paragraphs": 142,
  "total_sections": 6
}
```

**word_read_track_changes returns:**

```json
{
  "path": "~/Documents/Henderson/brief_v3.docx",
  "changes": [
    {
      "change_id": "tc_001",
      "type": "deletion",
      "author": "Jane Smith",
      "date": "2026-02-22T16:30:00Z",
      "original_text": "within two years of the alleged fraud",
      "location": {
        "paragraph_index": 14,
        "context": "...the claim must be filed within two years of the alleged fraud under..."
      }
    },
    {
      "change_id": "tc_002",
      "type": "insertion",
      "author": "Jane Smith",
      "date": "2026-02-22T16:30:00Z",
      "new_text": "within one year of discovery",
      "location": {
        "paragraph_index": 14,
        "context": "...the claim must be filed ^^^within one year of discovery^^^ under..."
      }
    }
  ],
  "total_changes": 12,
  "authors": ["Jane Smith"],
  "summary": { "insertions": 7, "deletions": 4, "format_changes": 1 }
}
```

**word_read_comments returns:**

```json
{
  "path": "~/Documents/Henderson/brief_v3.docx",
  "comments": [
    {
      "comment_id": "cmt_001",
      "author": "Jane Smith",
      "date": "2026-02-22T16:35:00Z",
      "text": "We should cite Morrison v. National here for the extraterritoriality point.",
      "anchor_text": "transactions occurring outside the United States",
      "paragraph_index": 22,
      "replies": [
        {
          "author": "Will Schall",
          "date": "2026-02-22T17:10:00Z",
          "text": "Agreed. I'll add the cite."
        }
      ]
    }
  ],
  "total_comments": 5
}
```

**word_create returns:**

```json
{
  "success": true,
  "path": "~/Documents/Henderson/brief_draft.docx",
  "total_paragraphs": 45,
  "file_size_bytes": 28400
}
```

**word_edit returns:**

```json
{
  "success": true,
  "path": "~/Documents/Henderson/brief_v3_elnor_working.docx",
  "edits_applied": 3,
  "saved_to": "~/Documents/Henderson/brief_v3_elnor_working.docx"
}
```

**word_compare returns:**

```json
{
  "success": true,
  "original_path": "~/Documents/Henderson/brief_v3.docx",
  "revised_path": "~/Documents/Henderson/brief_v3_elnor_working.docx",
  "output_path": "~/Documents/Henderson/brief_v3_tracked.docx",
  "author": "Elnor"
}
```

**word_accept_reject_changes returns:**

```json
{
  "success": true,
  "path": "~/Documents/Henderson/brief_v3_tracked.docx",
  "action": "accept_by_author",
  "author": "Elnor",
  "changes_modified": 5,
  "saved_to": "~/Documents/Henderson/brief_v3_reviewed.docx"
}
```

**Error returns (any tool):**

```json
{
  "error": true,
  "tool": "read_track_changes.py",
  "message": "File not found: ~/Documents/Henderson/brief_v3.docx"
}
```

### §3.6 Scripts to Implement

The `scripts/` directory contains the actual implementation. Python scripts use python-docx for .docx file manipulation. AppleScript files handle operations that require Word's UI (Compare, track changes toggle, open in app).

**Python scripts (python-docx based):**

| Script | Operation | Notes |
|--------|-----------|-------|
| `create.py` | Create new .docx from structured content | Supports template-based creation |
| `read.py` | Extract full text | Optionally includes formatting markers |
| `read_structure.py` | Extract headings/sections as outline | Used for navigation before editing |
| `read_track_changes.py` | Parse .docx XML for track change elements | Read-only: parses `w:ins`, `w:del`, `w:rPr` change tags |
| `read_comments.py` | Parse comments and replies | Maps comment anchors to document text |
| `edit.py` | Apply clean edits (no revision marks) | Works at python-docx API level, no XML manipulation needed |
| `insert_comment.py` | Add comment at anchor location | Writes to comments.xml and adds range markers |
| `find_replace.py` | Find/replace across all paragraphs | Handles runs split across XML elements |
| `format.py` | Apply inline and paragraph formatting | Modifies `w:rPr` and `w:pPr` elements |
| `style_create.py` | Create named paragraph style | Adds to styles.xml |
| `style_apply.py` | Apply style to paragraphs | Sets `w:pStyle` reference |
| `headers_footers.py` | Set headers/footers with placeholders | Handles section-specific headers |
| `insert_section.py` | Insert section break | Creates new `w:sectPr` |
| `insert_list.py` | Insert numbered/bulleted list | Handles numbering definitions |
| `insert_signature_block.py` | Insert formatted signature block | Right-aligned, spaced lines |
| `highlight.py` | Apply highlight color to text | Sets `w:highlight` in run properties |
| `save_as.py` | Copy file to new path | Simple file copy with validation |
| `copy_content.py` | Copy paragraphs between documents | Preserves formatting and styles |
| `export_pdf.py` | Convert .docx to PDF | Uses LibreOffice headless or pandoc |
| `accept_reject_changes.py` | Accept/reject tracked changes programmatically | Modifies XML: accept removes `w:ins` wrapper, reject removes `w:del` content |

**AppleScript files (require Word running):**

| Script | Operation | Notes |
|--------|-----------|-------|
| `open_in_app.applescript` | Open document in Word | `tell application "Microsoft Word" to open` |
| `compare.applescript` | Compare two documents to generate tracked changes | Uses Word's Compare Documents. Produces real Accept/Reject revision marks. This is how Elnor generates track changes — NOT via XML authoring. |
| `toggle_track_changes.applescript` | Toggle track changes state | Requires document to be open in Word |

**Requirements for each script:**
- Accept JSON parameters as first argument
- Return valid JSON to stdout
- Return structured error JSON on failure (not stack traces)
- Handle missing files, locked files, corrupted documents gracefully
- python-docx scripts must not require Word to be running
- AppleScript scripts require Word to be running and will return clear error if it's not

### §3.7 Implementation Notes

**Track changes are generated by Word Compare, not by XML authoring.** The `compare.applescript` tells Word to compare the original document against the revised copy. Word generates proper `w:ins`/`w:del` revision marks with author and date attributes. This produces identical output to what you'd get if someone edited with Track Changes turned on — each change can be individually Accepted or Rejected in Word's Review tab.

**Why not direct XML authoring?** python-docx does not support authoring tracked changes at a high level. Writing `w:ins`/`w:del` XML directly is fragile: Word is picky about surrounding run structure, change IDs, timestamps, and how revisions interact with styles/fields. A small XML mistake makes Word silently discard revisions or show "corrupted content" prompts. Word Compare eliminates this entire class of bugs.

**Accepting/rejecting track changes programmatically** is still done via XML manipulation (in `accept_reject_changes.py`): accepting an insertion removes the `w:ins` wrapper and keeps the content; accepting a deletion removes the `w:del` element and its content entirely. This is read-then-modify, which is much simpler than authoring new tracked changes from scratch. The script must handle nested changes (e.g., a deletion inside an insertion by another author).

**Find/replace across runs.** In .docx format, a single visible word might be split across multiple XML `w:r` (run) elements due to spelling checks, formatting changes, or track change history. The find/replace script must handle cross-run text matching.

**The working copy rule.** The edit workflow always creates a copy before modifying anything. The original file is never touched. This is enforced structurally: `edit.py` checks that the input path contains `_elnor_working` or is in a designated working directory. If the path appears to be an original (no working suffix, in a case folder or OneDrive sync path), the script returns an error: "Cannot edit original file directly. Use word_save_as to create a working copy first."

**Headers/footers with placeholders.** `{page}` and `{pages}` map to Word's `PAGE` and `NUMPAGES` field codes in the XML. `{filename}` maps to the `FILENAME` field code. The script writes proper field code XML, not literal text.

**PDF export** uses LibreOffice headless (`soffice --headless --convert-to pdf`) as python-docx cannot render PDFs. This requires LibreOffice installed on the Mac. Alternative: AppleScript to tell Word to save as PDF (but requires Word running).

**Template-based creation.** `word_create` can accept a `template` path — it opens the template .docx and inserts content into it, preserving the template's styles, headers, footers, and formatting. This is how you'd use a firm letterhead or brief template.

**Attachment-to-Word pipeline.** Combined with the Outlook skill's `email_save_attachments`, Elnor can: receive email → save .docx attachment → create working copy → edit → generate tracked changes via Compare → export PDF → draft reply email with both attached. This is the primary autonomous document workflow.

### §3.8 Endpoints, commands, and implementation notes

**No new Word-specific HTTP endpoint is required for MVP execution.** Word execution remains wrapper-script driven.

**Suggested capability manifest example**
```json
{
  "capability_id": "word.compare_documents",
  "family": "word",
  "kind": "wrapper_script",
  "title": "Generate tracked changes via Word Compare",
  "origin_skill": "word",
  "aliases": ["compare word documents", "generate tracked changes"],
  "action_verbs": ["compare", "diff"],
  "surface_order": ["applescript"],
  "runtime_binding": {
    "binding_kind": "script",
    "target": "skills/word/scripts/compare.applescript"
  },
  "required_runtime_caps": ["word_running"],
  "permissions": ["automation:Microsoft Word"],
  "requires_supervision": false,
  "dry_run_supported": false,
  "routing_eligible": true,
  "verification": [
    { "kind": "file_hash", "target": "output_path" },
    { "kind": "window_state", "target": "Microsoft Word" }
  ],
  "health_probe": {
    "kind": "command",
    "target": "osascript ~/.openclaw/workspace/skills/word/scripts/open_in_app.applescript '{\"path\":\"/tmp/test.docx\"}'",
    "timeout_ms": 5000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```
## §4 Legal Tables Skill (TOC & TOA)

### §4.1 Overview


### §4.1A Capability family and control surfaces

**Capability family:** `legal_tables`

**Primary control surfaces (in order):**
1. `python_wrapper` for scan/preview/config
2. `applescript` / Word VBA macro bridge for TOC/TOA generation and tweaks
3. `bridge_tool` for EC standing orders, corrections, capsules, and learning writes

**Required capability manifests (minimum)**
```text
skills/legal-tables/capabilities/toc_scan.json
skills/legal-tables/capabilities/toc_generate.json
skills/legal-tables/capabilities/toa_scan.json
skills/legal-tables/capabilities/toa_generate.json
skills/legal-tables/capabilities/toa_tweak.json
skills/legal-tables/capabilities/configure.json
skills/legal-tables/control-surface.json
```

**Control-surface policy**
- Citation recognition stays with the model plus its legal knowledge, standing orders, corrections, and current matter context.
- Mechanical insertion, pagination, and field-code generation stay with the Word/VBA layer.
- User corrections after generation become learning/proposal signals through EC; they do not mutate permanent conventions silently.

**Health probe**
```json
{
  "kind": "command",
  "target": "python3 ~/.openclaw/workspace/skills/legal-tables/scripts/configure.py '{\"action\":\"get\"}'",
  "timeout_ms": 5000
}
```

A specialized skill for generating Tables of Contents and Tables of Authorities in legal briefs. This is separate from the generic Word skill because it involves domain-specific workflow orchestration (citation recognition, legal categorization, jurisdiction-aware formatting, iterative user review) that doesn't belong in a generic document tool.

The Legal Tables skill works in three layers:
- **The agent (LLM)** handles citation recognition, short-form matching, and categorization — the intelligent work that the model already knows how to do from training on legal text
- **The Legal Tables skill** handles workflow orchestration, user review cycles, formatting configuration, and VBA macro invocation
- **VBA macros in a Word template** handle the mechanical Word operations (TA field codes, TOC field codes, section breaks, pagination field configuration)

**Path:** `~/.openclaw/workspace/skills/legal-tables/`

**Depends on:**
- Word skill (§3) for document reading and manipulation
- `Elnor_Legal.dotm` Word template installed in Word's startup folder (contains VBA macros)
- elnor-ec skill (DOC4 §3) for standing orders, corrections, and learning signals

### §4.2 Citation Style Configuration

The skill supports multiple citation style manuals. The active style determines how the agent identifies, formats, and categorizes citations.

| Style | Use For | Notes |
|-------|---------|-------|
| `bluebook` | Federal court filings | The Bluebook (current edition). Standard for all federal courts. Default. |
| `california` | California state court filings | California Style Manual (4th ed.). Permitted alongside Bluebook in CA courts, but CA courts prefer CSM. Key differences: date placement after case name, parentheses around full citation, official reporters required (Cal., Cal.App.) instead of West regional reporters, no spaces in abbreviations (F.Supp. not F. Supp.), supra used for case short cites. |
| `texas` | Texas state court filings | The Greenbook. |
| `custom` | Any jurisdiction with specific rules | User teaches Elnor via standing orders and corrections. |

**Style is set per document or per task.** The agent asks which style to use if it's not obvious from context (e.g., if the document is clearly for SDNY, use Bluebook). The style can also be set as a standing order: "All Henderson filings use Bluebook" or "California state filings use California Style Manual."

### §4.3 User Preference Overrides

Citation manuals often allow options in areas where practice varies. The Bluebook, for example, permits both italics and underlining for case names. Users may also have firm conventions that deviate from the manual's default presentation, or simply prefer one accepted variant over another. The skill must respect these preferences consistently.

**How preferences work:**

Preferences are stored as EC standing orders or corrections, not as skill configuration files. This means they flow through the existing learning system (DOC1 §6 `elnor_learn`, DOC2 §2 standing orders) and apply everywhere Elnor handles citations — not just in TOA generation. If you tell Elnor you prefer italics in a TOA review, he'll also italicize case names when drafting a brief section, because the preference lives in EC memory, not in a Legal Tables config.

**Examples of preferences the user can set:**
- "I prefer case names italicized, not underlined"
- "Always use full parallel citations for California cases, not just the official reporter"
- "Use 'Sec.' not '§' for statute sections"
- "In the TOA, list Constitutional provisions before Statutes"
- "Short-cite Henderson cases as 'Henderson, supra' not 'Henderson, 842 F.3d at'"
- "Despite the Bluebook, don't include parallel cites for Supreme Court cases"
- "Use 'F. Supp. 3d' not 'F.Supp.3d' — I know the Bluebook says close up but I want spaces"
- "For unpublished opinions, always include the Westlaw cite"

**Three ways to set preferences:**

1. **Tell Elnor in conversation.** "From now on, always italicize case names." Elnor calls `elnor_learn` with a correction signal. EC stores it as a standing order. Applies globally to all future legal work.

2. **Correct during TOA/TOC review.** "That citation should be italicized, not underlined." Elnor learns the preference automatically — the correction signal fires on every review-cycle correction.

3. **Create a standing order directly.** In Q or via conversation: "Standing order: Citation Preferences — case names italicized, not underlined; parallel cites for all California cases; 'passim' threshold is 5 pages." Stored in EC standing orders.

The agent loads standing orders and corrections via `elnor_standing_orders` and `elnor_corrections` before every legal tables task (per SKILL.md instructions). User preferences override manual defaults. Manual defaults override nothing — they're the fallback when no preference exists.

**Preference hierarchy:**
1. User's explicit preference (standing order or learned correction) — always wins
2. Active style manual's rule (Bluebook, CSM, etc.) — used when no user preference
3. Skill default — fallback of last resort

### §4.4 SKILL.md

```markdown
---
name: legal-tables
description: Generate Tables of Contents and Tables of Authorities for legal briefs. TOC, TOA, citations, Bluebook, California Style Manual, jurisdiction formatting, user review.
allowed-tools: Bash(python3:*) Bash(osascript:*) Read Write
---

# Legal Tables — TOC & TOA Generation

Generate professional Tables of Contents and Tables of Authorities
for litigation briefs. Supports Bluebook, California Style Manual,
and custom citation styles with iterative user review.

Scripts are in this skill's scripts/ directory. Call via:
`python3 scripts/<script>.py '<json_params>'`
VBA macros via: `osascript scripts/run_macro.applescript '<json_params>'`

## Before Starting: Load Context
1. Read knowledge/standing-orders.md — loads citation preferences, style rules
2. Read knowledge/corrections.md — loads past citation corrections for this domain
3. Check if there's an active capsule for the case — load it for context
4. Apply preference hierarchy: user preference > style manual > default

## Citation Recognition
You already know Bluebook and California Style Manual citation formats
from your training. Use that knowledge to identify citations.
For unfamiliar formats or jurisdictions, ask the user or search for
the applicable rules.

When identifying citations, look for:
- Full case citations (party names, volume, reporter, page, court, year)
- Statute citations (title, code, section)
- Rule citations (FRCP, FRE, local rules)
- Constitutional provisions
- Treatise citations (author, title, section)
- Regulation citations (CFR, state regs)
- Short forms: supra, id., hereinafter, shortened party names
- Match every short form to its full citation

## Table of Contents Workflow
1. SCAN: `python3 scripts/toc_scan.py '{"path": "..."}'` to read document headings
2. PREVIEW: `python3 scripts/toc_preview.py '{"headings": [...], "style": "..."}'` to format for human visibility and later review
   - Show all headings with levels
   - Show pagination scheme
   - Flag anything that looks structurally uncertain
3. GENERATE: Insert the TOC via VBA macro:
   `osascript scripts/run_macro.applescript '{"macro": "Elnor_InsertTOC", "params": {...}}'`
4. REPORT: Tell user what was generated: "Generated TOC for Henderson brief.
   12 headings across 4 levels. Roman numeral front matter, arabic body.
   Take a look — let me know if anything needs adjusting."
5. LEARN: If user provides corrections, log each via elnor_learn (correction signal).
   Apply corrections to the current document immediately.

## Table of Authorities Workflow
1. SCAN: Read the document with Word skill's `word_read`. Identify ALL legal citations
   yourself. Then call `python3 scripts/toa_scan.py '{"citations": [...], "path": "..."}'`
   to validate structure and map pages.
2. GENERATE: Insert the TOA via VBA macro:
   `osascript scripts/run_macro.applescript '{"macro": "Elnor_InsertTOA", "params": {...}}'`
3. REPORT: Tell user what was generated. Include a summary:
   "Generated TOA for Henderson brief. Found 23 case citations, 8 statutes,
   3 rules, 2 constitutional provisions. 4 citations flagged as uncertain
   (see below). Take a look — let me know if anything needs adjusting."
   List any uncertain identifications with confidence notes.
4. LEARN: If user provides corrections, log each via elnor_learn (correction signal).
   Apply corrections immediately and re-generate the affected portion.

## Self-Learning
Every correction during review is a learning opportunity:
- Log the correction via elnor_learn (correction signal)
- Apply the correction to the current document immediately
- The correction informs future TOA/TOC generation automatically
- Citation format preferences, category preferences, style overrides
  all persist as standing orders for future work

## User Preference Overrides
Users may prefer conventions that differ from the style manual's default.
These preferences ALWAYS override the manual. Examples:
- Italics vs underline for case names
- Parallel citation inclusion/exclusion
- Section symbol vs "Sec." for statutes
- Category ordering in the TOA
- Short-cite format preferences

Check standing orders before applying any default. If no preference
exists, follow the active style manual.

## Model Selection
Citation scanning benefits from a high-quality model. If EC model routing
is available, defer to EC's recommendation for this task. The user can
also specify a model preference for legal tables work via standing order.

## CRITICAL
- ALWAYS load standing orders and corrections before starting
- ALWAYS respect user preference overrides even when they deviate from
  the citation manual's default (e.g., italics vs underline)
- A misidentified citation in a TOA is a professional embarrassment —
  flag anything you're uncertain about with confidence level and note
- After generation, report what was done and flag any uncertain items
- User reviews output after generation, not before. Corrections feed
  back into learning for future tasks.
- For v1, mark full citations only. Do NOT auto-mark short forms like
  "id." or "supra" unless they can be unambiguously anchored to a
  specific full citation with high confidence. Short-form auto-marking
  is Phase 2.
```

### §4.5 Script Interface Reference

> **OpenClaw reconciliation:** Same as §2.3 — these are implementation specs for Python scripts the agent calls via `exec`, not formal tool definitions.

```json
{
  "tools": [
    {
      "name": "legal_toc_scan",
      "description": "Scan a legal brief's heading structure for Table of Contents generation. Returns detected headings with levels, paragraph indices. Agent should review and present to user before generating.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Path to the .docx file"
          }
        },
        "required": ["path"]
      }
    },
    {
      "name": "legal_toc_preview",
      "description": "Generate a formatted TOC preview for user review. Shows exactly what the TOC will look like with the given headings and pagination scheme. Returns formatted text preview that agent presents to the user.",
      "parameters": {
        "type": "object",
        "properties": {
          "headings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": { "type": "string", "description": "Heading text" },
                "level": { "type": "number", "description": "1 = top level, 2 = sub, 3 = sub-sub" },
                "paragraph_index": { "type": "number" },
                "include": { "type": "boolean", "description": "Include in TOC. Default: true." }
              }
            },
            "description": "Headings from legal_toc_scan with any user corrections"
          },
          "pagination": {
            "type": "string",
            "enum": ["roman_then_arabic", "no_numbers_then_arabic", "consecutive", "custom"],
            "description": "Pagination scheme for TOC/TOA pages vs brief pages"
          },
          "custom_pagination": {
            "type": "object",
            "properties": {
              "toc_page_format": { "type": "string", "enum": ["roman_lower", "roman_upper", "arabic", "none"] },
              "toc_start_number": { "type": "number" },
              "brief_page_format": { "type": "string", "enum": ["arabic", "roman_lower", "roman_upper"] },
              "brief_start_number": { "type": "number" }
            },
            "description": "Custom pagination config. Only used when pagination is 'custom'."
          },
          "toc_title": {
            "type": "string",
            "description": "Title for the TOC page. Default: 'TABLE OF CONTENTS'"
          },
          "style": {
            "type": "string",
            "enum": ["bluebook", "california", "texas", "custom"],
            "description": "Citation style. Affects formatting conventions. Default: bluebook."
          }
        },
        "required": ["headings"]
      }
    },
    {
      "name": "legal_toc_generate",
      "description": "Insert a Table of Contents into the document. Creates a new section with configured pagination. Requires Word running (uses VBA macro). Generates immediately and reports what was created; user may review and tweak after generation.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "headings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": { "type": "string" },
                "level": { "type": "number" },
                "paragraph_index": { "type": "number" },
                "include": { "type": "boolean" }
              }
            },
            "description": "Approved headings from user review"
          },
          "insert_location": {
            "type": "string",
            "enum": ["document_start", "after_cover_page", "before_paragraph"],
            "description": "Where to insert the TOC section. Default: document_start."
          },
          "insert_before_paragraph": {
            "type": "number",
            "description": "Paragraph index. Only used when insert_location is 'before_paragraph'."
          },
          "pagination": { "type": "string", "enum": ["roman_then_arabic", "no_numbers_then_arabic", "consecutive", "custom"] },
          "custom_pagination": { "type": "object" },
          "toc_title": { "type": "string" },
          "style": { "type": "string", "enum": ["bluebook", "california", "texas", "custom"] },
          "save_to": { "type": "string", "description": "Save to this path. Default: overwrite original." }
        },
        "required": ["path", "headings"]
      }
    },
    {
      "name": "legal_toc_tweak",
      "description": "Make specific adjustments to an existing TOC without regenerating from scratch. Add, remove, re-level, rename, or reorder entries. Update pagination. Requires Word running.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file with existing TOC" },
          "tweaks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "action": { "type": "string", "enum": ["add", "remove", "change_level", "rename", "reorder", "change_pagination"] },
                "heading_text": { "type": "string", "description": "Text of the heading to modify" },
                "new_text": { "type": "string", "description": "For rename action" },
                "new_level": { "type": "number", "description": "For change_level action" },
                "move_to": { "type": "string", "enum": ["up", "down"], "description": "For reorder action" },
                "pagination": { "type": "string", "description": "For change_pagination action" }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "tweaks"]
      }
    },
    {
      "name": "legal_toa_scan",
      "description": "Package the agent's citation analysis for the review workflow. The AGENT identifies citations by reading the document (via word_read) and using its legal training knowledge. This tool structures that analysis, validates format, and maps citations to page locations in the document. The agent passes in its identified citations; the tool prepares them for preview.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file (for page location mapping)" },
          "citations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "citation_id": { "type": "string", "description": "Unique ID for this citation (e.g., 'cite_001')" },
                "full_citation": { "type": "string", "description": "Full citation text as it should appear in the TOA" },
                "category": {
                  "type": "string",
                  "enum": ["cases", "constitutional_provisions", "statutes", "rules", "regulations", "treatises", "other_authorities"],
                  "description": "Authority category"
                },
                "short_forms": {
                  "type": "array",
                  "items": { "type": "string" },
                  "description": "All short forms found in the document for this citation (supra, id., shortened names, etc.)"
                },
                "page_occurrences": {
                  "type": "array",
                  "items": { "type": "number" },
                  "description": "Page numbers where this citation or its short forms appear"
                },
                "passim": {
                  "type": "boolean",
                  "description": "True if citation appears on 5+ pages (use 'passim' instead of page list). Threshold configurable."
                },
                "confidence": {
                  "type": "string",
                  "enum": ["high", "medium", "low"],
                  "description": "Agent's confidence in correct identification and categorization"
                },
                "confidence_note": {
                  "type": "string",
                  "description": "If medium or low, explain why (e.g., 'Unsure if Henderson refers to Henderson v. Pacific or Henderson v. Allied')"
                }
              }
            },
            "description": "All citations identified by the agent from reading the document"
          },
          "style": {
            "type": "string",
            "enum": ["bluebook", "california", "texas", "custom"],
            "description": "Citation style the document uses"
          }
        },
        "required": ["path", "citations"]
      }
    },
    {
      "name": "legal_toa_preview",
      "description": "Generate a formatted TOA preview for user review. Shows citations organized by category with page references. Returns formatted text that the agent presents to the user for approval, with review options.",
      "parameters": {
        "type": "object",
        "properties": {
          "citations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "citation_id": { "type": "string" },
                "full_citation": { "type": "string" },
                "category": { "type": "string" },
                "page_occurrences": { "type": "array", "items": { "type": "number" } },
                "passim": { "type": "boolean" },
                "confidence": { "type": "string" },
                "confidence_note": { "type": "string" },
                "short_forms": { "type": "array", "items": { "type": "string" } }
              }
            },
            "description": "Citations from legal_toa_scan (with any user corrections applied)"
          },
          "category_order": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Order of category sections in the TOA. Default: ['cases', 'constitutional_provisions', 'statutes', 'rules', 'regulations', 'treatises', 'other_authorities']"
          },
          "category_titles": {
            "type": "object",
            "description": "Custom display titles for categories. Default: {'cases': 'Cases', 'constitutional_provisions': 'Constitutional Provisions', 'statutes': 'Statutes', 'rules': 'Rules', 'regulations': 'Regulations', 'treatises': 'Treatises', 'other_authorities': 'Other Authorities'}"
          },
          "formatting": {
            "type": "object",
            "properties": {
              "case_name_style": { "type": "string", "enum": ["italic", "underline", "bold"], "description": "How to format case names. Default: italic." },
              "page_reference_style": { "type": "string", "enum": ["dotted_leader", "right_aligned", "inline"], "description": "How page numbers appear. Default: dotted_leader." }
            }
          },
          "toa_title": {
            "type": "string",
            "description": "Title for the TOA page. Default: 'TABLE OF AUTHORITIES'"
          },
          "style": { "type": "string", "enum": ["bluebook", "california", "texas", "custom"] },
          "pagination": { "type": "string", "enum": ["roman_then_arabic", "no_numbers_then_arabic", "consecutive", "custom"] }
        },
        "required": ["citations"]
      }
    },
    {
      "name": "legal_toa_generate",
      "description": "Insert a Table of Authorities into the document. Marks citation occurrences with TA field codes and generates the formatted TOA table. Creates a new section with configured pagination. Requires Word running (uses VBA macros). Generates immediately and reports uncertain items for post-generation review.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file" },
          "citations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "citation_id": { "type": "string" },
                "full_citation": { "type": "string" },
                "category": { "type": "string" },
                "short_forms": { "type": "array", "items": { "type": "string" } },
                "page_occurrences": { "type": "array", "items": { "type": "number" } },
                "passim": { "type": "boolean" }
              }
            },
            "description": "Approved citations from user review"
          },
          "insert_location": {
            "type": "string",
            "enum": ["after_toc", "document_start", "before_paragraph"],
            "description": "Where to insert TOA section. Default: after_toc (if TOC exists) or document_start."
          },
          "insert_before_paragraph": { "type": "number" },
          "category_order": { "type": "array", "items": { "type": "string" } },
          "category_titles": { "type": "object" },
          "formatting": {
            "type": "object",
            "properties": {
              "case_name_style": { "type": "string", "enum": ["italic", "underline", "bold"] },
              "page_reference_style": { "type": "string", "enum": ["dotted_leader", "right_aligned", "inline"] }
            }
          },
          "toa_title": { "type": "string" },
          "style": { "type": "string", "enum": ["bluebook", "california", "texas", "custom"] },
          "pagination": { "type": "string", "enum": ["roman_then_arabic", "no_numbers_then_arabic", "consecutive", "custom"] },
          "custom_pagination": { "type": "object" },
          "save_to": { "type": "string" }
        },
        "required": ["path", "citations"]
      }
    },
    {
      "name": "legal_toa_tweak",
      "description": "Make specific adjustments to an existing TOA without regenerating from scratch. Correct citations, change categories, adjust formatting, add or remove entries. Requires Word running.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path to .docx file with existing TOA" },
          "tweaks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "enum": ["correct_citation", "change_category", "remove", "add", "change_formatting", "reorder_categories", "change_case_name_style"]
                },
                "citation_id": { "type": "string", "description": "Citation to modify (for correct/change/remove actions)" },
                "new_citation_text": { "type": "string", "description": "For correct_citation action" },
                "new_category": { "type": "string", "description": "For change_category action" },
                "new_citation": {
                  "type": "object",
                  "description": "For add action — full citation object with citation_id, full_citation, category, short_forms, page_occurrences, passim"
                },
                "formatting_change": { "type": "object", "description": "For change_formatting action" },
                "new_category_order": { "type": "array", "items": { "type": "string" }, "description": "For reorder_categories action" },
                "new_style": { "type": "string", "enum": ["italic", "underline", "bold"], "description": "For change_case_name_style action" }
              }
            }
          },
          "save_to": { "type": "string" }
        },
        "required": ["path", "tweaks"]
      }
    },
    {
      "name": "legal_tables_configure",
      "description": "Get or set the default configuration for legal tables generation. 'get' returns current defaults. 'set' persists new defaults AND logs them as a standing order via elnor_learn so they apply across sessions.",
      "parameters": {
        "type": "object",
        "properties": {
          "action": { "type": "string", "enum": ["get", "set"] },
          "config": {
            "type": "object",
            "properties": {
              "default_style": { "type": "string", "enum": ["bluebook", "california", "texas", "custom"] },
              "default_pagination": { "type": "string", "enum": ["roman_then_arabic", "no_numbers_then_arabic", "consecutive"] },
              "default_case_name_style": { "type": "string", "enum": ["italic", "underline"] },
              "default_toc_title": { "type": "string" },
              "default_toa_title": { "type": "string" },
              "default_category_order": { "type": "array", "items": { "type": "string" } },
              "default_model_preference": { "type": "string", "description": "Preferred model for citation scanning step" },
              "passim_threshold": { "type": "number", "description": "Number of pages before using 'passim'. Default: 5." }
            },
            "description": "Configuration values. For 'set' action."
          }
        },
        "required": ["action"]
      }
    }
  ]
}
```

### §4.6 Bridge Pattern (Deprecated — See §1.4)

> **OpenClaw reconciliation:** **Deprecated.** Agent calls scripts directly via `exec`. No `index.js` needed. Code preserved as reference only.

```javascript
// Legal Tables skill bridge
// Orchestrates TOC/TOA workflows using Word skill tools and VBA macros.
// Citation identification is done by the agent (LLM), not this code.

const { execFile } = require('child_process');
const path = require('path');

const SCRIPTS_DIR = path.join(__dirname, 'scripts');
const TIMEOUT_SCAN_MS = 15000;     // scanning large documents
const TIMEOUT_GENERATE_MS = 30000; // VBA macro execution (TOA with many citations)
const TIMEOUT_TWEAK_MS = 15000;

function runScript(scriptName, params = {}, timeout = TIMEOUT_SCAN_MS) {
  return new Promise((resolve) => {
    const scriptPath = path.join(SCRIPTS_DIR, scriptName);
    const input = JSON.stringify(params);

    execFile('python3', [scriptPath, input], { timeout }, (err, stdout, stderr) => {
      if (err) {
        resolve({
          error: true,
          tool: scriptName,
          message: `Legal tables script failed: ${err.message}`,
          stderr: stderr?.trim() || null
        });
        return;
      }
      try {
        resolve(JSON.parse(stdout));
      } catch {
        resolve({
          error: true,
          tool: scriptName,
          message: 'Failed to parse script response',
          raw_output: stdout?.substring(0, 500) || null
        });
      }
    });
  });
}

function runVBAMacro(macroName, params = {}) {
  return new Promise((resolve) => {
    const scriptPath = path.join(SCRIPTS_DIR, 'run_macro.applescript');
    const input = JSON.stringify({ macro: macroName, params });

    execFile('osascript', [scriptPath, input],
      { timeout: TIMEOUT_GENERATE_MS },
      (err, stdout, stderr) => {
        if (err) {
          resolve({
            error: true,
            tool: macroName,
            message: `VBA macro failed: ${err.message}. Is Word running with Elnor_Legal.dotm loaded?`,
            stderr: stderr?.trim() || null
          });
          return;
        }
        try {
          resolve(JSON.parse(stdout));
        } catch {
          resolve({ success: true, output: stdout?.trim() || null });
        }
      }
    );
  });
}

module.exports = {
  // Scanning and preview (python-based, no Word UI needed)
  legal_toc_scan:          (p) => runScript('toc_scan.py', p),
  legal_toc_preview:       (p) => runScript('toc_preview.py', p),
  legal_toa_scan:          (p) => runScript('toa_scan.py', p),
  legal_toa_preview:       (p) => runScript('toa_preview.py', p),
  legal_tables_configure:  (p) => runScript('configure.py', p),

  // Generation and tweaks (VBA macro via AppleScript — requires Word running)
  legal_toc_generate:      (p) => runVBAMacro('ELNOR_GenerateTOC', p),
  legal_toc_tweak:         (p) => runVBAMacro('ELNOR_TweakTOC', p),
  legal_toa_generate:      (p) => runVBAMacro('ELNOR_GenerateTOA', p),
  legal_toa_tweak:         (p) => runVBAMacro('ELNOR_TweakTOA', p),
};
```

### §4.7 VBA Macro Package

**File:** `Elnor_Legal.dotm` — Word template with VBA macros

**Installation:** Placed in Word's startup folder on macOS:
`~/Library/Group Containers/UBF8T346G9.Office/User Content/Startup/Word/`

When Word launches with this template in the startup folder, the macros become available globally to all documents. The agent invokes them via AppleScript's `run VBA macro` command.

**Macros to implement:**

| Macro | Purpose | Input | Notes |
|-------|---------|-------|-------|
| `ELNOR_GenerateTOC` | Insert section break, set pagination, insert TOC field codes, format title | JSON: headings, pagination, toc_title, style, insert_location | Creates new section for TOC with independent headers/footers |
| `ELNOR_TweakTOC` | Modify existing TOC entries without regenerating | JSON: tweaks array | Updates TOC field and refreshes |
| `ELNOR_MarkCitations` | Insert TA (Table of Authorities) field codes at every occurrence of each citation in the document | JSON: citations array with full_citation, short_forms, category index | Core mechanical work — finds each full cite and short form text, wraps in `{ TA \l "full_citation" \s "short_form" \c category_number }` |
| `ELNOR_GenerateTOA` | Mark all citations (calls ELNOR_MarkCitations), insert section break, set pagination, insert TOA field, format category headings | JSON: citations, formatting, pagination, category_order, category_titles | Orchestrates full TOA generation |
| `ELNOR_TweakTOA` | Modify existing TOA entries without regenerating | JSON: tweaks array | Correct TA fields, change categories, add/remove, then update |
| `ELNOR_UpdateFields` | Refresh all field codes (page numbers, TOC, TOA) | None | Run after any structural change to update page numbers |
| `ELNOR_SetPagination` | Configure section-specific page numbering | JSON: section_index, format (roman/arabic/none), start_number | Called by TOC/TOA generate; also available standalone |

**Macro input/output:** Each macro accepts a JSON string parameter (passed via AppleScript → Word VBA `Run` method) and returns a JSON result string. Errors return `{ "error": true, "message": "..." }`.

**TA field code format:**
```
{ TA \l "Morrison v. National Australia Bank Ltd., 561 U.S. 247 (2010)" \s "Morrison" \c 1 }
```
Where `\l` is the long (full) citation, `\s` is the short form, and `\c` is the category number (1=Cases, 2=Statutes, etc.). Each occurrence of the citation in the document gets wrapped with a TA field.

**VBA macro requirements:**
- Handle documents with existing TOC/TOA (replace, not duplicate)
- Handle section breaks correctly (TOC/TOA in their own section with independent pagination)
- Handle different pagination schemes per section (roman numerals for table pages, arabic for brief body, no numbers for cover page)
- Use proper TA field code format for each citation category
- Handle passim correctly (the field code still marks every occurrence; Word's TOA field auto-generates "passim" when occurrences exceed threshold)
- Apply formatting from configuration (italic/underline for case names, dotted leaders for page references)
- Update all fields after generation so rendered page numbers are correct
- Handle documents with continuous section breaks (don't insert unnecessary page breaks)

### §4.8 TOA Workflow Detail

The full Table of Authorities workflow, updated for R7’s autonomy-first posture:

**Step 1 — Agent reads the document and context**
```text
word_read(path) → full text
elnor_standing_orders() → citation preferences / style overrides
elnor_corrections() → prior corrections
capsule/context card (if matter-scoped) → current matter context
```

**Step 2 — Agent identifies citations**
The model identifies citations using:
- its legal citation knowledge,
- active matter context,
- standing orders,
- prior corrections,
- and the selected style manual.

Each citation gets:
- full citation text,
- category,
- page occurrences,
- short forms (if safely attributable),
- confidence,
- uncertainty note (when needed).

**Step 3 — Agent structures the analysis**
```text
legal_toa_scan(path, citations, style)
```

**Step 4 — Agent generates the TOA**
```text
legal_toa_generate(path, citations, formatting, pagination, style)
```

**Step 5 — Agent reports what was generated**
Example:
```text
Generated TOA for Henderson brief.
Found 23 case citations, 8 statutes, 3 rules, and 2 constitutional provisions.
4 entries are flagged as uncertain:
- SEC v. Dimensional Holdings Corp. — likely missing WL cite
- [etc.]

Take a look and tell me what should change.
```

**Step 6 — User reviews after generation**
The user can:
- approve the output implicitly by moving on,
- ask for a correction,
- ask Elnor to fix a citation,
- change category ordering,
- change formatting rules,
- add/remove an authority.

**Step 7 — Corrections become learning**
Every accepted correction creates:
- an immediate tweak to the current document,
- a learning/correction signal to EC,
- and, when repeated, a capability or workflow proposal.

**Conservative v1 rule**
Short forms such as `id.` and `supra` are not auto-marked unless they can be anchored to a full citation with high confidence.


### §4.8A Learning, proposals, and capability integration

**This skill is now explicitly connected to the capability-learning substrate.**

- Corrections from TOC/TOA review become EC learning signals and, when repeated, capability proposals.
- User preference changes (case-name style, category order, passim threshold, parallel-citation rules) are stored through EC standing orders / corrections, not as silent ad hoc edits.
- Future promotion targets include:
  - new jurisdiction profiles,
  - refined citation matching heuristics,
  - category ordering preferences,
  - safer short-form expansion rules.

**Suggested capability manifest example**
```json
{
  "capability_id": "legal_tables.toa_generate",
  "family": "legal_tables",
  "kind": "wrapper_script",
  "title": "Generate Table of Authorities",
  "origin_skill": "legal-tables",
  "aliases": ["generate TOA", "make table of authorities"],
  "action_verbs": ["generate", "insert", "format"],
  "surface_order": ["python_wrapper", "applescript"],
  "runtime_binding": {
    "binding_kind": "workflow_pattern",
    "target": "legal-tables::toa_generate"
  },
  "required_runtime_caps": ["word_running", "elnor_legal_dotm_loaded"],
  "permissions": ["automation:Microsoft Word"],
  "requires_supervision": false,
  "dry_run_supported": false,
  "routing_eligible": true,
  "verification": [
    { "kind": "tool_result", "target": "citation_count" },
    { "kind": "window_state", "target": "Microsoft Word" }
  ],
  "health_probe": {
    "kind": "command",
    "target": "python3 ~/.openclaw/workspace/skills/legal-tables/scripts/configure.py '{\"action\":\"get\"}'",
    "timeout_ms": 5000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §4.9 Scripts to Implement

**Python scripts (document analysis, preview generation):**

| Script | Purpose | Notes |
|--------|---------|-------|
| `toc_scan.py` | Parse .docx heading structure using python-docx | Returns headings with levels, paragraph indices. Detects Heading 1/2/3 styles and manual bold/caps that look like headings. |
| `toc_preview.py` | Format a TOC preview as text from heading list + pagination config | Pure formatting — takes structured input, returns readable preview string. |
| `toa_scan.py` | Accept agent's citation list, validate structure, map citations to page locations | Page mapping: uses paragraph-to-page estimation from python-docx, or calls Word via AppleScript for precise page numbers if Word is running. |
| `toa_preview.py` | Format a TOA preview as text from citation list + formatting config | Groups by category, applies formatting indicators (italic markers), generates dotted leaders. |
| `configure.py` | Read/write legal tables defaults | Stored as JSON at `~/.openclaw/workspace/skills/legal-tables/config.json`. On `set`, also triggers elnor_learn to persist as standing order. |

**AppleScript (VBA macro bridge):**

| Script | Purpose | Notes |
|--------|---------|-------|
| `run_macro.applescript` | Generic macro runner | Opens Word if needed, verifies Elnor_Legal.dotm is loaded, runs named VBA macro with JSON params string, returns result. |

### §4.10 Implementation Notes

**Page number mapping.** The .docx format does not contain true page boundaries — page breaks depend on rendering (font metrics, margins, printer driver). For accurate page-to-citation mapping:
- **Python estimate:** Count paragraphs, estimate ~50 lines per page. Adequate for preview phase.
- **Word accurate:** When Word is running, AppleScript can query `page number of range of paragraph X` for precise results. The VBA macros always have accurate page numbers because they run inside Word.
- The preview uses estimates. The generated TOA uses Word's live field resolution for actual page numbers.

**Passim threshold.** Configurable via `legal_tables_configure` (default: 5). If a citation appears on 5+ pages, the TOA entry shows "passim" instead of listing every page number. This is standard Bluebook practice (Rule 2.4). Word's TOA field code supports passim natively via the `\p` switch.

**Category ordering.** Configurable per document and per user default. Standard Bluebook ordering: Cases, Constitutional Provisions, Statutes, Rules, Regulations, Treatises, Other Authorities. The user can reorder at preview time or set a standing order for their preferred ordering (e.g., "Treatises above Rules").

**Category numbers for TA fields.** Word's TA field uses numeric category codes (`\c 1` through `\c 16`). The mapping from our category names to Word's category numbers must be consistent. Default mapping: 1=Cases, 2=Constitutional Provisions, 3=Statutes, 4=Rules, 5=Regulations, 6=Treatises, 7=Other Authorities.

**Multiple tables in one document.** Complex filings often have both TOC and TOA (and sometimes a Table of Exhibits). The workflow handles this: generate TOC first, then TOA after it. Both can share one front-matter section or use separate sections. The pagination configuration applies per section.

**Existing TOC/TOA replacement.** If the document already has a TOC or TOA, the generate tools replace it rather than creating a duplicate. Detection: look for existing TOC/TOA field codes in the document. The tweak tools modify in place without regenerating.

**Section break mechanics.** The tables go in a section with independent headers/footers so pagination can differ from the brief body. The VBA macro inserts a section break (next page), configures the new section's page number format and starting number, and sets up headers/footers independently from the following section.

**Pagination schemes explained:**
- `roman_then_arabic`: Table pages numbered i, ii, iii... Brief body starts at page 1. Most common for federal court.
- `no_numbers_then_arabic`: Table pages have no page numbers displayed. Brief body starts at page 1. Some courts prefer this.
- `consecutive`: Entire document numbered 1, 2, 3... continuously. Simple but less common for briefs with tables.
- `custom`: User specifies format and start number for each section independently.

---


---


---
## §5 Browser Skill (General)

### §5.1 Overview


### §5.1A Capability family and control surfaces

### §5.1B Browser demotion when structured connectors exist

The browser skill remains critical, but it is now explicitly a **lower-preference remote-system path** when a healthy connector/API/MCP surface already exists.

**Rule**
- If a supported connector can access the live system of record, prefer that connector over browser automation.
- Use the browser skill when:
  - there is no good structured path,
  - the web UI exposes capabilities the connector does not,
  - or the task is inherently a browser/UI behavior rather than data retrieval.

**Capability family:** `browser`

**DOC3 role:** knowledge layer over the native OpenClaw browser tool.

**Primary control surfaces (in order):**
1. `native_openclaw_browser`
2. `page_knowledge_ui`
3. `raw_ui` only when browser-native control genuinely cannot reach the surface

**Required capability manifests (minimum)**
```text
skills/browser/capabilities/session_check.json
skills/browser/capabilities/site_check.json
skills/browser/capabilities/guard_check.json
skills/browser/control-surface.json
skills/browser/page-knowledge/*.json
skills/browser/ui-anchors/*.json
skills/browser/site-registry.json
```

**Rules**
- Browser execution itself remains OpenClaw-native.
- Browser “helper tools” in this document are workflow patterns, not separately registered runtime tools.
- Managed `openclaw` profile is the default for autonomous work.
- Extension relay is secondary for already-open user tabs and requires explicit attachment.

The Browser skill is a **knowledge layer** on top of OpenClaw's native `browser` tool. OpenClaw already provides full browser control via CDP (Chrome DevTools Protocol) with Playwright — the agent can navigate, snapshot, click, type, screenshot, and execute JavaScript. What the agent lacks is **site-specific knowledge**: which elements matter, what workflows look like, and what to watch out for (like charge popups).

This skill provides that knowledge. Site-specific skills (like Westlaw) build on top of it.

**Path:** `~/.openclaw/workspace/skills/browser/`

**Depends on:** OpenClaw's native `browser` tool (built-in, not a workspace skill).

### §5.2 OpenClaw's Native Browser Tool

The agent interacts with the browser through OpenClaw's built-in tool surface. Understanding this surface is essential — the browser skill wraps it, not replaces it.

**Core interaction loop:**

```
1. openclaw browser snapshot --interactive    → returns element refs (e1, e2, e3...)
2. openclaw browser click e12                 → clicks element e12
3. openclaw browser snapshot --interactive    → refs are STALE — must re-snapshot
4. openclaw browser type e15 "search terms"   → types into element e15
5. openclaw browser snapshot --interactive    → re-snapshot again
```

**Critical rule:** Element refs (e1, e2, etc.) go stale after **every** navigation, click, or DOM change. The agent MUST re-snapshot after every action before taking the next one.

**Available commands:**

| Command | What It Does |
|---------|-------------|
| `openclaw browser snapshot --interactive` | Scan page, return interactive elements with refs (e1, e2...) |
| `openclaw browser snapshot --interactive --compact` | Same but shorter output (fewer tokens) |
| `openclaw browser snapshot --labels` | Screenshot with overlaid ref labels |
| `openclaw browser click e12` | Click element by ref |
| `openclaw browser click e12 --double` | Double-click |
| `openclaw browser type e15 "text"` | Type into element |
| `openclaw browser type e15 "text" --submit` | Type and press Enter |
| `openclaw browser type e15 "text" --clear` | Clear field first, then type |
| `openclaw browser fill --field "name:value" --field "email:value"` | Batch fill multiple fields |
| `openclaw browser press Enter` | Press a key |
| `openclaw browser navigate https://url` | Navigate to URL |
| `openclaw browser open https://url` | Open URL in new tab |
| `openclaw browser tabs` | List open tabs |
| `openclaw browser focus <tabId>` | Switch to tab |
| `openclaw browser close <tabId>` | Close tab |
| `openclaw browser screenshot` | Viewport screenshot |
| `openclaw browser screenshot --full-page` | Full scrollable page |
| `openclaw browser screenshot --ref 12` | Screenshot specific element |
| `openclaw browser wait "#selector"` | Wait for element |
| `openclaw browser wait --load networkidle` | Wait for network idle |
| `openclaw browser evaluate "js expression"` | Execute JavaScript in page context |
| `openclaw browser cookies --json` | Read cookies |
| `openclaw browser requests --filter api --json` | Read network requests |

**All commands accept `--browser-profile <name>`** to target a specific browser mode. All accept `--json` for machine-readable output.

### §5.3 Browser Modes

OpenClaw supports two browser modes. Both use the same tool surface (snapshot/click/type/etc.). The difference is what browser instance they control.

#### §5.3.1 Managed Browser (`openclaw` profile) — Primary Mode

A dedicated, isolated Chromium instance with its own user-data directory. OpenClaw launches and controls it autonomously. No extension needed, no manual clicking to attach.

```json
// ~/.openclaw/openclaw.json
{
  "browser": {
    "enabled": true,
    "defaultProfile": "openclaw",
    "headless": false,
    "profiles": {
      "openclaw": {
        "cdpPort": 18800,
        "color": "#FF4500"
      }
    }
  }
}
```

**Start:** `openclaw browser --browser-profile openclaw start`
**Stop:** `openclaw browser --browser-profile openclaw stop`

**Login handling:** You log in to sites manually once in the managed browser window. Cookies and sessions persist in the dedicated user-data directory (`~/.openclaw/browser/openclaw/user-data/`). When sessions expire, you log in again manually.

**Why this is the primary mode:**
- No manual per-tab attachment
- Full autonomous control — Elnor can navigate, open tabs, switch tabs without user intervention
- Isolated from your personal browsing — can't accidentally access personal tabs/cookies
- Sessions persist across restarts

**Tradeoffs:**
- Separate from your daily-driver browser — no access to your personal bookmarks, extensions, or logged-in sessions
- Must log in to sites manually (once) in this browser

#### §5.3.2 Extension Relay (`chrome` profile) — Secondary Mode

Controls tabs in your real, daily-driver Chrome browser. Requires the OpenClaw Browser Relay extension installed, and you must **click the extension icon on each tab** you want Elnor to control.

```
# Install extension (one-time)
openclaw browser extension install
openclaw browser extension path   # → load unpacked at chrome://extensions

# Use it
# 1. Open the tab you want controlled
# 2. Click the extension icon (badge shows "ON")
# 3. Agent can now control that tab
```

**When to use extension relay:**
- You need Elnor to interact with a site where your personal login is required and re-logging-in in the managed browser is impractical
- The site uses device-bound authentication (hardware keys, device certificates) that can't be replicated in the managed browser
- You want Elnor to work with tabs you already have open

**Manual requirement:** You must click the extension icon per-tab. There is no way around this — it's a Chrome security feature. The extension uses `chrome.debugger` API which requires explicit user consent per tab.

**Security warning:** Extension relay gives the agent access to whatever the tab's logged-in session can access. Use a dedicated Chrome profile (separate from personal browsing) if possible.

#### §5.3.3 Switching Between Modes

The `--browser-profile` flag controls which mode is used per-command:

```bash
# Use managed browser (default if defaultProfile: "openclaw")
openclaw browser --browser-profile openclaw snapshot --interactive

# Use extension relay to your real Chrome
openclaw browser --browser-profile chrome snapshot --interactive
```

The browser skill and all site skills accept a `profile` parameter on every tool call. Default is whatever's set in `browser.defaultProfile`. To switch modes globally:

```json
// Switch to managed (autonomous, primary)
{ "browser": { "defaultProfile": "openclaw" } }

// Switch to extension relay (manual attach, secondary)
{ "browser": { "defaultProfile": "chrome" } }
```

You can also create named profiles for different purposes:

```bash
openclaw browser create-profile \
  --name westlaw-chrome \
  --driver extension \
  --cdp-url http://127.0.0.1:18792 \
  --color "#00AA00"
```

### §5.4 Architecture

```
browser/                    ← Browser knowledge layer (this section)
├── SKILL.md
├── scripts/
│   └── guard.py            ← Charge/popup guard (uses evaluate + snapshot)
├── site-registry.json      ← Maps domains → site-specific skills
└── README.md

westlaw/                    ← Site-specific knowledge + workflows (§6)
├── SKILL.md
├── page-knowledge/         ← What the agent should look for in snapshots
│   ├── login.json
│   ├── home.json
│   ├── search-results.json
│   ├── case-reading.json
│   └── litigation-analytics.json
├── scripts/
│   └── guard_westlaw.py    ← Westlaw-specific charge keyword scan
└── README.md

{future-site}/              ← Same pattern for any new site
├── SKILL.md
├── page-knowledge/
├── ...
```

**Key design decision:** Two-layer architecture. The browser skill provides mode management, the guard engine, and the site registry. Site skills provide **page knowledge** (what elements look like in snapshots, what text to expect, what workflows to follow) and **high-level tools** (like `westlaw_search`) that orchestrate multiple snapshot/act cycles internally.

### §5.5 Site Registry

```json
{
  "sites": [
    {
      "name": "westlaw",
      "domains": [
        "1.next.westlaw.com",
        "next.westlaw.com",
        "auth.thomsonreuters.com"
      ],
      "skill_path": "~/.openclaw/workspace/skills/westlaw",
      "requires_auth": true,
      "charge_guard": true,
      "preferred_profile": "openclaw",
      "notes": "Log in manually in managed browser. Session persists in user-data dir."
    }
  ]
}
```

Fields:
- `preferred_profile` — which browser mode this site works best with
- `charge_guard` — if true, the guard engine runs after every navigation and action on this site
- `requires_auth` — if true and session is expired, prompt user to log in manually (managed mode) or verify the tab is attached (extension mode)

When a new site is mapped:
1. Create a new skill folder following the §5.4 structure
2. Add an entry to `site-registry.json`
3. The browser skill automatically loads page knowledge when navigating to a registered domain

### §5.6 SKILL.md

```markdown
---
name: browser
description: Site-aware browser knowledge layer over OpenClaw's native browser tool. Web page automation, charge guard, site registry.
allowed-tools: Bash(python3:*) Read
---

# Browser Knowledge Layer

This skill provides site-specific knowledge and safety guards on top of
OpenClaw's native `browser` tool.

## You Already Have Browser Tools
OpenClaw's built-in `browser` tool handles all direct interaction:
snapshot, click, type, navigate, screenshot, wait, evaluate.
DO NOT duplicate these — use them directly.

## What This Skill Adds
- **Site registry** — knows which sites have dedicated skills
- **Guard engine** — blocks charge popups and dangerous modals
- **Mode management** — switches between managed browser and extension relay

## Before Any Browser Task
1. Check if the target site has a dedicated skill (e.g., Westlaw → use westlaw skill).
2. If on a charge-guarded site, run browser_guard_check after navigation.

## Browser Modes
- **Managed browser** (profile: "openclaw") — primary, autonomous, no manual steps
- **Extension relay** (profile: "chrome") — secondary, requires manual tab attachment

## CRITICAL RULES
- NEVER approve charges, fees, or payments on any site.
- NEVER click through terms/conditions without user approval.
- For login: use credentials saved in the browser profile only when the relevant site policy in this document explicitly allows it. If the managed browser has saved credentials for a site, enter them to log in. If no saved credentials exist, tell the user to log in manually. If CAPTCHA or unusual verification appears, pause and notify the user.
- After EVERY click or navigation, re-snapshot before the next action.
  Refs go stale immediately.
```

### §5.7 Skill Tools (Inline Agent Behaviors)

> **OpenClaw reconciliation:** These are NOT scripts and NOT formal tool definitions. They are workflow patterns the SKILL.md instructs the agent to follow using built-in tools (`browser`, `exec`, `read`). The agent performs these checks inline by calling native browser commands and reading `site-registry.json`. There are no separate runtime tools for these helpers.

> **Codex note:** Do NOT create script files for `browser_guard_check`, `browser_check_site`, or `browser_switch_profile`. These are agent-performed sequences using existing OpenClaw commands.

The browser skill only adds three tools — everything else uses OpenClaw's native `browser` tool directly.

```json
{
  "tools": [
    {
      "name": "browser_guard_check",
      "description": "Check the current page for charge dialogs, consent popups, or dangerous modals. Runs a snapshot + evaluate scan. Returns what was found and recommended action. Called automatically on charge-guarded sites, but can be called manually on any page.",
      "parameters": {
        "type": "object",
        "properties": {
          "auto_decline_charges": {
            "type": "boolean",
            "description": "If true, automatically click decline/cancel on charge dialogs. Default: true."
          },
          "profile": {
            "type": "string",
            "description": "Browser profile to use. Default: current default profile."
          }
        }
      }
    },
    {
      "name": "browser_check_site",
      "description": "Check what site the browser is on. Returns URL, title, whether it matches a registered site, whether a dedicated skill exists, session status, and preferred browser profile for the site.",
      "parameters": {
        "type": "object",
        "properties": {
          "profile": {
            "type": "string",
            "description": "Browser profile to check. Default: current default profile."
          }
        }
      }
    },
    {
      "name": "browser_switch_profile",
      "description": "Switch the active browser profile. Use to switch between managed browser and extension relay.",
      "parameters": {
        "type": "object",
        "properties": {
          "profile": {
            "type": "string",
            "description": "Profile name: 'openclaw' (managed), 'chrome' (extension relay), or a custom profile name."
          }
        },
        "required": ["profile"]
      }
    }
  ]
}
```

### §5.8 Guard Engine

The guard engine protects against unintended charges and unwanted agreements. It works within the snapshot/ref model.

**How it runs (two-phase scan):**

**Phase 1 — Snapshot scan:**
Take a snapshot (`openclaw browser snapshot --interactive`). Scan the returned text for charge-related keywords:

```
"charge", "charges", "additional cost", "extra cost", "premium",
"fee", "fees", "accept charges", "approve", "ancillary",
"transactional", "billing", "purchase", "payment", "cost of",
"will be charged", "agree to pay", "not included in your plan"
```

**False-positive mitigation:** Keyword scanning checks ONLY:
- elements with `role="dialog"` or `role="alertdialog"`
- visible overlays with z-index > 1000
- interactive elements near the top of the snapshot list

Keyword matches inside case text, article body text, or other page content are ignored. The guard only fires on modal/overlay elements.

Also look for dialog/modal indicators in snapshot output:
- Elements with `role="dialog"` or `role="alertdialog"`
- Elements labeled "modal", "overlay", "popup"

**Phase 2 — JavaScript confirmation** (if Phase 1 flags something):
Use `openclaw browser evaluate` to check for:

```javascript
// Check for modal overlays
document.querySelector('[role="dialog"], [role="alertdialog"], .modal, .overlay');
// Check z-index overlays
[...document.querySelectorAll('*')].filter(el =>
  getComputedStyle(el).zIndex > 1000 &&
  getComputedStyle(el).position !== 'static' &&
  el.offsetWidth > 100 && el.offsetHeight > 100
);
```

**Actions:**

| Finding | Action |
|---------|--------|
| Charge dialog found | Snapshot to find decline/cancel button ref → click it → log what was declined → report to user |
| Non-charge dialog found | Report to agent for evaluation |
| No dialog | Return `{clear: true}` |

**Logging:** Every blocked charge → `~/.openclaw/workspace/skills/browser/guard-log.json` with timestamp, site, dialog text, action taken.

**Guard NEVER approves charges.** If it can't find a decline button, it reports to the user and takes no action.

### §5.9 Page Knowledge Files

Site-specific skills include **page knowledge files** that help the agent interpret snapshots. These are NOT CSS selectors the tool uses directly — they describe what elements look like in OpenClaw's snapshot output so the agent can find the right ref.

**Standard format:**

```json
{
  "page": "search-results",
  "url_pattern": "*/Search/Results*",
  "how_to_identify": "Page title contains 'Search Results'. Snapshot shows a search input (textarea) and result title links.",
  "key_elements": {
    "search_input": {
      "snapshot_label": "textbox 'Enter search terms' or similar",
      "role": "textbox",
      "css_id": "#searchInputId",
      "notes": "The main search bar at top. In snapshot, look for a textbox near the top of the element list."
    },
    "search_button": {
      "snapshot_label": "button 'Search'",
      "role": "button",
      "css_id": "#searchButton",
      "notes": "Search submit button."
    },
    "result_links": {
      "snapshot_label": "link '{case name}' — multiple, numbered",
      "role": "link",
      "css_id_pattern": "#cobalt_result_case_title{N}",
      "notes": "Case title links in results list. N = 1, 2, 3... First result is result 1."
    }
  },
  "shadow_dom_elements": {
    "description": "Some elements on this page use SAF web components with shadow DOM. OpenClaw snapshots may not see inside these. Use `evaluate` to reach them.",
    "example": "document.querySelector('[data-testid=\"claims-textarea\"]').shadowRoot.querySelector('textarea')"
  }
}
```

**Why both `snapshot_label` and `css_id`?**
- `snapshot_label` is what the agent sees in snapshot output — this is the primary identifier
- `css_id` is backup knowledge for `evaluate` fallback when snapshot refs can't reach the element (shadow DOM cases)
- Over time, snapshot labels may change as Westlaw updates its UI — the self-healing pattern (§5.10) handles this

### §5.10 Self-Healing

When the agent can't find an expected element in a snapshot:

1. The expected `snapshot_label` from page knowledge doesn't match anything in the snapshot
2. Agent takes a `--labels` screenshot to visually inspect the page
3. Agent identifies the new label/role for the element
4. Agent proposes an update to the page knowledge file
5. User approves
6. Agent calls `elnor_learn` (DOC2) to store the correction
7. Page knowledge file is updated

This means page knowledge improves over time as sites update their UIs.

### §5.10A Implementation and ownership notes

**Runtime owner:** OpenClaw native browser tool.

**DOC3 owner responsibilities:**
- site registry,
- page-knowledge files,
- UI anchors,
- guard logic,
- site-specific workflows,
- capability manifests that describe browser-layer abilities without pretending to replace native browser execution.

**Suggested capability manifest example**
```json
{
  "capability_id": "browser.guard_check",
  "family": "browser",
  "kind": "workflow_pattern",
  "title": "Check the current browser page for charge or danger dialogs",
  "origin_skill": "browser",
  "aliases": ["check popup", "check charge guard", "inspect browser modal"],
  "action_verbs": ["check", "guard", "inspect"],
  "surface_order": ["native_openclaw_browser"],
  "runtime_binding": {
    "binding_kind": "workflow_pattern",
    "target": "browser::guard_check"
  },
  "required_runtime_caps": ["browser_available"],
  "permissions": [],
  "requires_supervision": false,
  "dry_run_supported": true,
  "routing_eligible": true,
  "verification": [
    { "kind": "tool_result", "target": "clear" }
  ],
  "health_probe": {
    "kind": "browser",
    "target": "snapshot",
    "timeout_ms": 3000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §5.11 Adding a New Site

When you want to map a new website for Elnor:

1. **Map the site** — Navigate through the site with Claude (in Chrome, or similar) and capture:
   - What elements look like in snapshots (roles, labels, text)
   - CSS IDs/selectors as backup for evaluate fallback
   - Shadow DOM components that snapshots can't reach
   - Workflows (login → search → read results → etc.)
   - Charge/payment dialogs to watch for

2. **Create the site skill folder:**
   ```
   ~/.openclaw/workspace/skills/{site-name}/
   ├── SKILL.md                ← Skill instructions + workflow descriptions
   ├── page-knowledge/
   │   ├── {page-name}.json    ← One file per page type
   │   └── ...
   ├── scripts/                ← Optional helper scripts
   └── README.md
   ```

3. **Add to site-registry.json** — Map domains, set auth/guard flags, set preferred profile.

4. **Iterate** — Use the self-healing pattern (§5.10) to refine page knowledge as you use the skill.

---
## §6 Westlaw Skill

### §6.1 Overview


### §6.1A Capability family and control surfaces

### §6.1B Westlaw future connector/API path

Today Westlaw remains primarily a browser/page-knowledge skill family. If a supported official API or safe structured adapter becomes available, Westlaw capabilities should adopt the same hybrid-control ordering used elsewhere:
1. structured API / connector for search, metadata, or document retrieval,
2. browser/page-knowledge for UI-only workflows,
3. raw UI only as a last resort.

Until then, the browser remains the main route and must keep strong page knowledge, session handling, and charge-guard behavior.

**Capability family:** `westlaw`

**Primary control surfaces (in order):**
1. `native_openclaw_browser`
2. `page_knowledge_ui`
3. `bridge_tool` (future Westlaw API connector, not MVP)
4. `raw_ui` only if browser-native control cannot complete the path

**Required capability manifests (minimum)**
```text
skills/westlaw/capabilities/session_check.json
skills/westlaw/capabilities/search.json
skills/westlaw/capabilities/read_results.json
skills/westlaw/capabilities/open_result.json
skills/westlaw/capabilities/read_case.json
skills/westlaw/capabilities/keycite.json
skills/westlaw/capabilities/judge_search.json
skills/westlaw/capabilities/judge_overview.json
skills/westlaw/capabilities/judge_motions.json
skills/westlaw/capabilities/know_your_judge.json
skills/westlaw/capabilities/deep_research.json
skills/westlaw/control-surface.json
skills/westlaw/config.json
```

**Rules**
- Westlaw remains browser-native for MVP.
- Managed `openclaw` profile with persisted session is the primary mode.
- Extension relay is allowed for a user’s already-open real browser tab when explicitly chosen.
- Default read output is narrowed to header + synopsis + holdings + KeyCite.
- Full opinion extraction happens only on explicit request or when a specific passage is required.
- Charge guard only evaluates visible dialogs/overlays, not opinion text.

The Westlaw skill provides high-level legal research tools that orchestrate OpenClaw's native browser commands with Westlaw-specific page knowledge. Each tool encapsulates a multi-step workflow (snapshot → find ref → act → re-snapshot → repeat) so the agent can execute complex research tasks without manually managing refs.

**Path:** `~/.openclaw/workspace/skills/westlaw/`

**Depends on:** Browser skill (§5), OpenClaw's native `browser` tool.

**Domain:** Thomson Reuters Westlaw Advantage (`1.next.westlaw.com`, `next.westlaw.com`, `auth.thomsonreuters.com`).

### §6.2 SKILL.md

```markdown
---
name: westlaw
description: Legal research on Westlaw Advantage — search cases, read opinions, analyze judges, KeyCite, litigation analytics, Know Your Judge. Westlaw, case law, judge research.
allowed-tools: Read
---

# Westlaw Legal Research

Use the workflows in this skill for ALL Westlaw research tasks.
Each workflow below orchestrates OpenClaw's built-in `browser` tool
(snapshot → find ref → act → re-snapshot) with Westlaw-specific page knowledge.
Read the page-knowledge/ files for element identification guidance.

## Before Any Westlaw Task
1. Call westlaw_check_session to verify you have an active session.
2. If the session is expired and `allow_saved_profile_relogin` is enabled for the managed browser profile, attempt a supervised re-login using saved browser autofill / saved client ID only.
3. If no saved-profile relogin is configured, or if re-login fails, tell the user: "Please log in to Westlaw in the managed browser and let me know when you're ready."
4. If CAPTCHA or unusual verification appears, pause and notify the user.

## Research Workflow
- Find a case → westlaw_search → westlaw_read_results → westlaw_open_result
- Read a case → westlaw_read_case (returns header, synopsis, holdings, opinion)
- Judge research → westlaw_judge_search → westlaw_judge_overview
- Motion data → westlaw_judge_motions (with case type filters)
- "How does Judge X handle [issue]" → westlaw_know_your_judge
- Deep research report → westlaw_deep_research
- Analyze a brief → westlaw_analyze_document

## Browser Mode
Default: managed browser (profile: "openclaw").
If user has attached Westlaw in extension relay, pass profile: "chrome".
The site registry (§5.5) records the preferred profile.

## CRITICAL RULES
- **NEVER approve charges.** The guard engine blocks charge popups automatically. If you see charge-related text in a visible dialog or overlay, stop and report to the user.
- Use saved-profile re-login only if explicitly enabled in config and only in the managed browser. If unusual verification or CAPTCHA appears, pause and notify the user.
- All write operations to Westlaw (saving, notes, folders) require user confirmation first.
- Default output for case reading is header + synopsis + holdings + KeyCite status. Full opinion text only when explicitly requested.
- Search results and case text are copyrighted. Provide citations and summaries to the user, not verbatim text dumps.
```

### §6.3 Authentication & Session Management

**Primary mode: managed browser with persisted session.** Westlaw is best used in the managed `openclaw` browser profile, where cookies and session state persist in the dedicated user-data directory.

**Default policy**
- Initial login is manual.
- Ongoing session reuse is automatic through persisted cookies.
- Optional saved-profile re-login may be enabled for the managed browser, but it is not required for MVP and must fail safely.
- If unusual verification or CAPTCHA appears, Elnor pauses and tells the user.

**Credential and config handling**
`skills/westlaw/config.json`
```json
{
  "client_id": "XXXXX",
  "default_sections": ["header", "synopsis", "holdings", "keycite"],
  "max_opinion_length": 5000,
  "snapshot_defaults": "--interactive --compact",
  "allow_saved_profile_relogin": false
}
```

If `allow_saved_profile_relogin` is set to `true`, Elnor may use saved browser autofill and the configured client ID when the login page is reached in the managed browser. This is a convenience mode, not a guarantee, and it must stop immediately if unexpected verification appears.

**Managed browser flow (primary)**
1. User logs in manually the first time in the managed browser.
2. Session cookies persist in `~/.openclaw/browser/openclaw/user-data/`.
3. Elnor reuses the active session automatically.
4. If the session expires:
   - if config allows saved-profile relogin, Elnor may attempt it once;
   - otherwise Elnor asks the user to log in manually.

**Extension relay flow (secondary)**
1. User logs in to Westlaw in their real Chrome/Chromium browser.
2. User attaches the OpenClaw relay to the tab.
3. Elnor controls only that attached tab.
4. If the relay detaches, DOC11 should report the capability as unavailable in the current session.

**Session detection (`westlaw_check_session`)**
The tool / workflow checks:
- active search/home indicators → session active
- `auth.thomsonreuters.com` → login required
- client ID prompt → client ID step required
- session-expired or redirect page → expired session
- CAPTCHA / unusual verification → manual user action required


### §6.4 Charge Guard (Westlaw-Specific)

In addition to the general guard engine (§5.8), the Westlaw skill adds site-specific charge keywords to the scan:

```
"ancillary charges", "transactional charges",
"this document is not included in your plan",
"premium content", "additional charges will apply",
"KeyCite" (in charge context)
```

**Scope:** Charge keywords are checked ONLY in dialog/modal elements (`role="dialog"`, `role="alertdialog"`, or visible overlays with z-index > 1000`). Keyword matches in opinion body text, case headers, or search results are ignored.

**Behavior:**
1. After every navigation and action on Westlaw, the guard scans the snapshot
2. If charge dialog found → find decline/cancel ref in snapshot → click it → log → report to user
3. Agent tells user: "Westlaw wanted to charge for [description]. I declined. Let me know if you want me to proceed anyway."
4. If user says "go ahead" → agent explains the charge explicitly and requires confirmation: "This will incur an additional charge of [description] on your Westlaw account. Confirm?"

### §6.5 Page Knowledge

Page knowledge files describe what Westlaw pages look like in OpenClaw snapshots. They live in `page-knowledge/` and follow the format from §5.9. The agent uses these to find the right refs in snapshot output.

All page knowledge below was mapped from a live Westlaw session. These will evolve via self-healing (§5.10) as Westlaw updates its UI.

#### §6.5.1 Login Pages (`page-knowledge/login.json`)

```json
{
  "page": "login",
  "url_patterns": [
    "auth.thomsonreuters.com/u/login/identifier",
    "auth.thomsonreuters.com/u/login/password"
  ],
  "how_to_identify": "URL contains auth.thomsonreuters.com. Snapshot shows email or password input and 'Sign in' button.",
  "key_elements": {
    "email_input":       { "snapshot_label": "textbox 'Email address'", "css_id": "#username" },
    "password_input":    { "snapshot_label": "textbox (password type)", "css_id": "#password" },
    "sign_in_button":    { "snapshot_label": "button 'Sign in'", "css_selector": "button[name='action']" },
    "onepass_link":      { "snapshot_label": "link 'Sign in with OnePass'", "css_id": "#innerEscapeHatchUrl" },
    "client_id_input":   { "snapshot_label": "textbox (client ID)", "css_id": "#co_clientIDTextbox" },
    "client_id_continue":{ "snapshot_label": "button 'Start new session' or 'Continue'", "css_id": "#co_clientIDContinueButton" }
  },
  "agent_action": "Do NOT fill these. Tell user to log in manually. These identifiers are here so the agent can recognize it's on a login page."
}
```

#### §6.5.2 Home / Search (`page-knowledge/home.json`)

```json
{
  "page": "home",
  "url_patterns": ["*/Search/Home*", "*/Analytics/Home*"],
  "how_to_identify": "Snapshot shows main search textbox, jurisdiction button, and navigation tabs (AI Deep Research, Precision Research, etc.).",
  "key_elements": {
    "search_input": {
      "snapshot_label": "textbox 'Enter search terms' or similar textarea",
      "css_id": "#searchInputId",
      "notes": "Main search bar. Takes terms, citations, databases, natural language."
    },
    "search_button": {
      "snapshot_label": "button 'Search' or 'Search Westlaw'",
      "css_id": "#searchButton"
    },
    "recent_searches": {
      "snapshot_label": "button with down-arrow icon near search bar",
      "css_id": "#co_searchLast10Link",
      "notes": "Opens recent searches dropdown"
    },
    "jurisdiction_button": {
      "snapshot_label": "button showing current jurisdiction name (e.g., 'All Federal', 'California')",
      "css_id": "#jurisdictionId"
    },
    "advanced_search_link": {
      "snapshot_label": "link 'Advanced'",
      "css_id": "#co_search_advancedSearchLink"
    }
  },
  "navigation_tabs": {
    "description": "8 tabs across the page below the search bar, all with role=tab",
    "tabs": {
      "ai_deep_research":   { "snapshot_label": "tab 'AI Deep Research'", "css_id": "#tab1" },
      "precision_research":  { "snapshot_label": "tab 'Precision Research'", "css_id": "#tab2" },
      "content_types":       { "snapshot_label": "tab 'Content types'", "css_id": "#tab3" },
      "federal":             { "snapshot_label": "tab 'Federal materials'", "css_id": "#tab4" },
      "state":               { "snapshot_label": "tab 'State materials'", "css_id": "#tab5" },
      "practice_areas":      { "snapshot_label": "tab 'Practice areas'", "css_id": "#tab6" },
      "my_content":          { "snapshot_label": "tab 'My content'", "css_id": "#tab7" },
      "tools":               { "snapshot_label": "tab 'Tools'", "css_id": "#tab8" }
    }
  },
  "deep_research_panel": {
    "input":        { "snapshot_label": "combobox or textbox in AI Deep Research panel", "css_id": "#deep-research-input", "data_testid": "input-textarea" },
    "time_selector":{ "snapshot_label": "button 'Agent Time' or '~10 min'", "css_id": "#report-type-menu-button" },
    "jurisdiction": { "snapshot_label": "button showing jurisdiction in DR panel", "data_testid": "jurisdictions-button" }
  },
  "typeahead": {
    "description": "Appears while typing in search bar. Shows suggestions grouped by source type.",
    "category_list":       { "css_id": "#typeAheadGroupList", "notes": "tablist with source type tabs" },
    "tab_suggestions":     { "css_id": "#Suggestions_Button" },
    "tab_cases":           { "css_id": "#Cases_Button" },
    "tab_statutes":        { "css_id": "#Statutes & Court Rules_Button" },
    "tab_regulations":     { "css_id": "#Regulations_Button" },
    "tab_secondary":       { "css_id": "#Secondary Sources_Button" },
    "judge_suggestion":    { "css_class": "SearchSuggestionsSection lajudge", "notes": "Judge name, title, court, location" },
    "case_suggestion":     { "css_class": "co-Typeahead-casesAnchorItem", "notes": "Case name, citation, date" }
  },
  "lit_doc_analyzer": {
    "notes": "SAF-ANCHOR shadow DOM elements. Snapshot may not see inside. Use text matching or evaluate.",
    "analyze_your_work":    { "text": "Analyze your work", "element_tag": "SAF-ANCHOR" },
    "analyze_opponent":     { "text": "Analyze an opponent's work", "element_tag": "SAF-ANCHOR" },
    "analyze_complaint":    { "text": "Analyze a complaint", "element_tag": "SAF-ANCHOR" },
    "get_judicial_insight": { "text": "Get judicial insight", "element_tag": "SAF-ANCHOR" }
  }
}
```

#### §6.5.3 Search Results (`page-knowledge/search-results.json`)

```json
{
  "page": "search-results",
  "url_patterns": ["*/Search/Results*"],
  "how_to_identify": "URL contains /Search/Results. Snapshot shows result title links, content type navigation, and sort dropdown.",
  "key_elements": {
    "content_type_dropdown": {
      "snapshot_label": "button 'All content' or current content type",
      "css_id": "#co_categorySearchButton"
    },
    "sort_dropdown": {
      "snapshot_label": "button 'Relevance' or current sort",
      "css_id": "#co_search_sortOptions_id"
    },
    "result_titles": {
      "snapshot_label": "link '{case name}' — multiple links in results list",
      "css_id_pattern": "#cobalt_result_case_title{N}",
      "notes": "N = 1, 2, 3... Click to open case. These are the main result links."
    },
    "result_citations": {
      "css_id_pattern": "#co_searchResults_citation_{N}",
      "notes": "Citation text near each result title"
    },
    "results_container": {
      "css_id": "#co_fermiSearchResult_data"
    }
  },
  "content_type_nav": {
    "description": "Left panel or top tabs showing content types with counts. Active type shown as bold text, others as links.",
    "types": {
      "overview":          { "css_id": "#co_search_contentNav_link_ALL" },
      "cases":             { "css_id": "#co_search_contentNav_link_CASE" },
      "statutes":          { "css_id": "#co_search_contentNav_link_STATUTE" },
      "secondary_sources": { "css_id": "#co_search_contentNav_link_ANALYTICAL" },
      "practical_law":     { "css_id": "#co_search_contentNav_link_KNOWHOW" },
      "regulations":       { "css_id": "#co_search_contentNav_link_REGULATION" },
      "public_records":    { "css_id": "#co_search_contentNav_link_PR-COUNTS" },
      "admin_decisions":   { "css_id": "#co_search_contentNav_link_ADMINDECISION" },
      "briefs":            { "css_id": "#co_search_contentNav_link_BRIEF" },
      "expert_materials":  { "css_id": "#co_search_contentNav_link_EXPERTWITNESS" },
      "forms":             { "css_id": "#co_search_contentNav_link_FORM" },
      "key_numbers":       { "css_id": "#co_search_contentNav_link_KEYNUMBER" },
      "trial_court_docs":  { "css_id": "#co_search_contentNav_link_PMM" },
      "all_results":       { "css_id": "#co_search_contentNav_link_ALL_RESULTS" }
    }
  },
  "filter_tabs": {
    "content_types_tab": { "css_id": "#tab-content-types" },
    "filters_tab":       { "css_id": "#tab-facets" }
  },
  "precision_filters": [
    "Legal issue & outcome", "Fact pattern", "Cause of action",
    "Motion type & outcome", "Governing law", "Industry type",
    "Party type", "Area of law", "Jurisdiction", "Reported Status", "Date"
  ],
  "pagination": {
    "results_per_page": 100,
    "next_page": { "snapshot_label": "link 'Next Page'" }
  }
}
```

#### §6.5.4 Case Reading (`page-knowledge/case-reading.json`)

```json
{
  "page": "case-reading",
  "url_patterns": ["*/Document/*/View/FullText*"],
  "how_to_identify": "URL contains /Document/ and /View/FullText. Snapshot shows case title heading, document tabs, and case text.",
  "key_elements": {
    "case_title": {
      "snapshot_label": "heading (h1) with case name",
      "css_id": "#co_docHeaderTitleLink"
    },
    "document_body": {
      "snapshot_label": "Large text region containing the opinion",
      "css_id": "#co_document",
      "notes": "Full case text. Very long. Use `evaluate` with innerText or OpenClaw's text extraction."
    },
    "download_pdf": {
      "snapshot_label": "link 'Download original image (PDF)' or similar",
      "css_class": "co_blobLink"
    }
  },
  "document_tabs": {
    "description": "Tabs above the document showing related information",
    "document":            { "snapshot_label": "tab 'Document'", "css_id": "#DocumentTab" },
    "filings":             { "snapshot_label": "tab 'Filings (N)'", "css_id": "#riFilingsTab" },
    "negative_treatment":  { "snapshot_label": "tab 'Negative Treatment'", "css_id": "#kcNegativeTreatmentTab" },
    "history":             { "snapshot_label": "tab 'History'", "css_id": "#kcJudicialHistoryTab" },
    "citing_references":   { "snapshot_label": "tab 'Citing References'", "css_id": "#kcCitingReferencesTab", "link": "#coid_relatedInfo_kcCitingReferences_link" },
    "cited_with":          { "snapshot_label": "tab 'Cited With'", "css_id": "#kcCoCitesTab" },
    "table_of_authorities":{ "snapshot_label": "tab 'Table of Authorities'", "css_id": "#kcTableOfAuthoritiesTab" }
  },
  "toolbar": {
    "toc_button":        { "snapshot_label": "button (hamburger icon for TOC)", "css_id": "#co_tocLink" },
    "prev_result":       { "css_id": "#co_documentFooterResultsNavigationPrevious" },
    "next_result":       { "css_id": "#co_documentFooterResultsNavigationNext" },
    "page_input":        { "css_id": "#co_document_starPage_starPageNavInput", "notes": "Star page jump input" },
    "page_go":           { "css_id": "#co_document_starPage_starPageNavGo" },
    "fullscreen":        { "css_id": "#co_fullscreenModeLink" }
  },
  "treatment_flags": {
    "yellow_flag": { "css_class": "co_yFlagLg", "meaning": "Caution — superseded or limited" },
    "red_flag":    { "css_class": "co_rFlagLg", "meaning": "Negative — overruled or reversed" },
    "blue_stripe": { "css_class": "icon_flag-blue", "meaning": "Appealed" },
    "green_c":     { "css_class": "co_gCLg", "meaning": "Positive — cited with approval" }
  },
  "treatment_notices": {
    "keycite_notice":     { "css_id": "#co_readingModeKC" },
    "negative_treatment": { "css_id": "#co_readingModeNegativeTreatment" }
  }
}
```

#### §6.5.5 Litigation Analytics (`page-knowledge/litigation-analytics.json`)

```json
{
  "page": "litigation-analytics",
  "url_patterns": ["*/Analytics/Home*", "*/Analytics/Profiler*"],
  "how_to_identify": "URL contains /Analytics/. Snapshot shows analytics search input and entity tabs (Attorneys, Law Firms, Judges, etc.).",
  "key_elements": {
    "analytics_search": {
      "snapshot_label": "textbox 'Search for judges' or similar",
      "css_id": "#la_searchInputId",
      "notes": "Auto-suggest after 3 characters. Judge suggestions show name, title, court."
    }
  },
  "entity_tabs": {
    "attorneys":  { "snapshot_label": "tab 'Attorneys'",  "css_id": "#tab-Attorneys" },
    "law_firms":  { "snapshot_label": "tab 'Law Firms'",  "css_id": "#tab-LawFirms" },
    "judges":     { "snapshot_label": "tab 'Judges'",     "css_id": "#tab-Judges" },
    "courts":     { "snapshot_label": "tab 'Courts'",     "css_id": "#tab-Courts" },
    "damages":    { "snapshot_label": "tab 'Damages'",    "css_id": "#tab-Damages" },
    "case_types": { "snapshot_label": "tab 'Case Types'", "css_id": "#tab-CaseTypes" },
    "companies":  { "snapshot_label": "tab 'Companies'",  "css_id": "#tab-Companies" }
  },
  "judge_profile_tabs": {
    "know_your_judge":   { "snapshot_label": "tab 'Know your judge'",    "css_id": "#co_la_knowYourJudge_tab" },
    "overview":          { "snapshot_label": "tab 'Overview'",           "css_id": "#co_la_profile_tab" },
    "experience":        { "snapshot_label": "tab 'Experience'",         "css_id": "#co_la_caseHistoryReport_tab" },
    "outcomes":          { "snapshot_label": "tab 'Outcomes'",           "css_id": "#co_la_partyOutcomeReport_tab" },
    "motions":           { "snapshot_label": "tab 'Motions'",            "css_id": "#co_la_motionReport_tab" },
    "precedent":         { "snapshot_label": "tab 'Precedent'",          "css_id": "#co_la_casePrecedentReport_tab" },
    "expert_challenges": { "snapshot_label": "tab 'Expert challenges'",  "css_id": "#co_la_expertsReport_tab" },
    "appeals":           { "snapshot_label": "tab 'Appeals'",            "css_id": "#co_la_appealsReport_tab" },
    "references":        { "snapshot_label": "tab 'References'",         "css_id": "#co_la_references_tab" }
  },
  "case_type_filter_checkboxes": {
    "description": "Checkboxes for filtering by case type. Pattern: #co_la_{camelCase}_checkbox. Has a type-ahead search input and 'Select all' checkbox.",
    "key_types": {
      "securities":   { "css_id": "#co_la_securitiesCommoditiesExchanges_checkbox", "label": "Securities / Commodities / Exchanges" },
      "class_action": { "css_id": "#co_la_classAction_checkbox",                    "label": "Class Action" },
      "civil_rights": { "css_id": "#co_la_civilRights_checkbox",                    "label": "Civil Rights" },
      "torts":        { "css_id": "#co_la_tortsNegligence_checkbox",                "label": "Torts & Negligence" },
      "labor":        { "css_id": "#co_la_laborEmployment_checkbox",                "label": "Labor & Employment" },
      "contracts":    { "css_id": "#co_la_contracts_checkbox",                      "label": "Contracts" },
      "ip_tech":      { "css_id": "#co_la_intellectualPropertyTechnology_checkbox", "label": "IP & Technology" },
      "rico":         { "css_id": "#co_la_rICO_checkbox",                           "label": "RICO" }
    },
    "apply_button": { "snapshot_label": "button 'Apply'" }
  },
  "know_your_judge_form": {
    "notes": "SAF web components with shadow DOM. Snapshot may NOT see inside these. Use `evaluate` to access inner textareas.",
    "claims_textarea": {
      "data_testid": "claims-textarea",
      "shadow_host": "[data-testid='claims-textarea']",
      "inner_element": "textarea",
      "evaluate_pattern": "document.querySelector('[data-testid=\"claims-textarea\"]').shadowRoot.querySelector('textarea')",
      "notes": "SAF-TEXT-AREA-V3. Max 500 chars."
    },
    "facts_textarea": {
      "data_testid": "facts-textarea",
      "shadow_host": "[data-testid='facts-textarea']",
      "inner_element": "textarea",
      "evaluate_pattern": "document.querySelector('[data-testid=\"facts-textarea\"]').shadowRoot.querySelector('textarea')",
      "notes": "SAF-TEXT-AREA-V3. Max 500 chars."
    },
    "specific_focus_textarea": {
      "data_testid": "specific-focus-textarea",
      "shadow_host": "[data-testid='specific-focus-textarea']",
      "inner_element": "textarea",
      "evaluate_pattern": "document.querySelector('[data-testid=\"specific-focus-textarea\"]').shadowRoot.querySelector('textarea')",
      "notes": "Optional. SAF-TEXT-AREA-V3. Max 500 chars."
    },
    "precedent_toggle": {
      "element_tag": "SAF-SWITCH-V3",
      "notes": "'Include precedent-based summary' toggle. Check aria-checked attribute."
    },
    "continue_button": {
      "data_testid": "continue-button",
      "element_tag": "SAF-BUTTON-V3"
    },
    "clear_button":     { "css_id": "#clear-filters-handler" },
    "new_query_button":  { "data_testid": "header-new-query-button", "element_tag": "SAF-BUTTON-V3" }
  }
}
```

### §6.6 Workflow Reference

> **OpenClaw reconciliation:** These are NOT registered tool definitions. They document the multi-step workflows the SKILL.md instructs the agent to perform using OpenClaw's built-in `browser` tool (snapshot, click, type, evaluate, etc.). Each "tool" below is a workflow pattern, not a callable function. The SKILL.md tells the agent when and how to execute each workflow.

Each tool internally orchestrates the snapshot → find ref → act → re-snapshot cycle. The agent calls the high-level tool; the tool handles the browser interaction mechanics.

```json
{
  "tools": [
    {
      "name": "westlaw_check_session",
      "description": "Check if Westlaw session is active. Takes a snapshot and checks for session indicators. Returns: 'active', 'login_required', 'client_id_required', or 'error'. If not active, tells user to log in manually.",
      "parameters": {
        "type": "object",
        "properties": {
          "profile": {
            "type": "string",
            "description": "Browser profile. Default: site registry preferred_profile."
          }
        }
      }
    },
    {
      "name": "westlaw_search",
      "description": "Search Westlaw. Navigates to home if needed, fills search bar, optionally sets content type and jurisdiction, then submits. Returns first page of results.",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Search query — terms, citations, natural language, or database identifiers"
          },
          "content_type": {
            "type": "string",
            "enum": ["cases", "statutes", "regulations", "secondary_sources", "briefs", "all"],
            "description": "Filter results by content type. Default: 'cases'."
          },
          "jurisdiction": {
            "type": "string",
            "description": "Jurisdiction filter (e.g., 'federal', 'california', '9th circuit'). Uses current jurisdiction if omitted."
          }
        },
        "required": ["query"]
      }
    },
    {
      "name": "westlaw_read_results",
      "description": "Read the current search results page. Snapshots the page and extracts result titles, citations, and available metadata.",
      "parameters": {
        "type": "object",
        "properties": {
          "max_results": {
            "type": "number",
            "description": "Maximum results to return from current page. Default: 10."
          },
          "include_synopsis": {
            "type": "boolean",
            "description": "Include synopsis/headnote text for each result. Default: false."
          }
        }
      }
    },
    {
      "name": "westlaw_open_result",
      "description": "Open a specific search result by its number (1-based). Finds the result title link ref in the snapshot and clicks it.",
      "parameters": {
        "type": "object",
        "properties": {
          "result_number": {
            "type": "number",
            "description": "Result number to open (1-based index on current page)"
          }
        },
        "required": ["result_number"]
      }
    },
    {
      "name": "westlaw_read_case",
      "description": "Read the currently open case document. Extracts structured data from the page: title, citation, court, date, synopsis, holdings, and opinion text.",
      "parameters": {
        "type": "object",
        "properties": {
          "sections": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Which sections to return: 'header', 'synopsis', 'holdings', 'headnotes', 'opinion', 'all'. Default: ['header', 'synopsis', 'holdings']."
          },
          "max_length": {
            "type": "number",
            "description": "Maximum characters for opinion text. Default: 20000."
          }
        }
      }
    },
    {
      "name": "westlaw_read_section",
      "description": "Read a specific section of the currently open case by scrolling to it or using star page navigation.",
      "parameters": {
        "type": "object",
        "properties": {
          "section": {
            "type": "string",
            "description": "Section heading or star page number to navigate to (e.g., 'Discussion', 'III.B', '315')"
          }
        },
        "required": ["section"]
      }
    },
    {
      "name": "westlaw_keycite",
      "description": "Access KeyCite information for the currently open case. Clicks the appropriate tab and reads treatment data.",
      "parameters": {
        "type": "object",
        "properties": {
          "tab": {
            "type": "string",
            "enum": ["negative_treatment", "history", "citing_references", "cited_with", "table_of_authorities"],
            "description": "Which KeyCite tab to read. Default: 'negative_treatment'."
          },
          "max_results": {
            "type": "number",
            "description": "Maximum citing references to return. Default: 20."
          }
        }
      }
    },
    {
      "name": "westlaw_copy_citation",
      "description": "Copy the citation of the currently open case.",
      "parameters": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": ["bluebook", "full", "short"],
            "description": "Citation format. Default: 'bluebook'."
          }
        }
      }
    },
    {
      "name": "westlaw_set_jurisdiction",
      "description": "Set the jurisdiction filter. Clicks the jurisdiction button and selects the target jurisdiction.",
      "parameters": {
        "type": "object",
        "properties": {
          "jurisdiction": {
            "type": "string",
            "description": "Jurisdiction name or abbreviation (e.g., 'federal', 'california', '9th circuit', 'all')"
          }
        },
        "required": ["jurisdiction"]
      }
    },
    {
      "name": "westlaw_next_page",
      "description": "Navigate to the next page of search results. Finds 'Next Page' link ref and clicks it.",
      "parameters": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "westlaw_judge_search",
      "description": "Search for a judge in Litigation Analytics. Types judge name in analytics search, waits for auto-suggest, clicks the matching judge link.",
      "parameters": {
        "type": "object",
        "properties": {
          "judge_name": {
            "type": "string",
            "description": "Judge name to search for"
          }
        },
        "required": ["judge_name"]
      }
    },
    {
      "name": "westlaw_judge_overview",
      "description": "Read the judge's Overview tab. Snapshots and extracts ruling tendencies, speed data, experience breakdown, contact info, and career timeline.",
      "parameters": {
        "type": "object",
        "properties": {
          "case_type_filter": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Filter by case type(s): 'securities', 'class_action', 'civil_rights', 'torts', 'labor', 'contracts', 'ip_tech', 'rico', etc."
          }
        }
      }
    },
    {
      "name": "westlaw_judge_motions",
      "description": "Read the judge's Motions tab. Extracts motion analytics: types, grant rates, time to rule, and individual motion details.",
      "parameters": {
        "type": "object",
        "properties": {
          "case_type_filter": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Filter by case type(s)"
          },
          "motion_type_filter": {
            "type": "string",
            "description": "Filter by motion type (e.g., 'Motion to Dismiss', 'Summary Judgment')"
          },
          "view": {
            "type": "string",
            "enum": ["chart", "table", "list"],
            "description": "What data to return. Default: 'table'."
          },
          "max_results": {
            "type": "number",
            "description": "Maximum individual motions to return from list. Default: 20."
          }
        }
      }
    },
    {
      "name": "westlaw_judge_outcomes",
      "description": "Read the judge's Outcomes tab. Extracts outcome analytics by case type and party.",
      "parameters": {
        "type": "object",
        "properties": {
          "case_type_filter": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Filter by case type(s)"
          },
          "view": {
            "type": "string",
            "enum": ["chart", "table", "list"],
            "description": "What data to return. Default: 'table'."
          },
          "max_results": {
            "type": "number",
            "description": "Maximum individual outcomes to return. Default: 20."
          }
        }
      }
    },
    {
      "name": "westlaw_know_your_judge",
      "description": "Use the Know Your Judge AI tool. Navigates to judge profile if needed, clicks Know Your Judge tab, fills the shadow DOM form using evaluate commands, and submits.",
      "parameters": {
        "type": "object",
        "properties": {
          "judge_name": {
            "type": "string",
            "description": "Judge to analyze. If not already on judge profile, searches first."
          },
          "claims": {
            "type": "string",
            "description": "Specific claims for the case (max 500 chars). Required."
          },
          "facts": {
            "type": "string",
            "description": "Key facts of the case (max 500 chars). Required."
          },
          "specific_focus": {
            "type": "string",
            "description": "Optional specific issue to focus on (max 500 chars)."
          },
          "include_precedent": {
            "type": "boolean",
            "description": "Include precedent-based summary. Default: true."
          }
        },
        "required": ["claims", "facts"]
      }
    },
    {
      "name": "westlaw_deep_research",
      "description": "Submit an AI Deep Research query. Clicks the AI Deep Research tab, fills the query input, optionally sets jurisdiction and agent time, then submits.",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Research question in natural language"
          },
          "jurisdiction": {
            "type": "string",
            "description": "Jurisdiction for the research"
          },
          "agent_time": {
            "type": "string",
            "enum": ["~5 min", "~10 min", "~15 min"],
            "description": "How long the AI agent should research. Default: '~10 min'."
          }
        },
        "required": ["query"]
      }
    },
    {
      "name": "westlaw_analyze_document",
      "description": "Use the Litigation Document Analyzer. Clicks the appropriate analyzer link (SAF-ANCHOR shadow DOM element, targeted by text content) and follows the upload flow.",
      "parameters": {
        "type": "object",
        "properties": {
          "analysis_type": {
            "type": "string",
            "enum": ["your_work", "opponent_work", "complaint", "judicial_insight"],
            "description": "Type of analysis to perform"
          },
          "document_path": {
            "type": "string",
            "description": "Path to the document to upload"
          }
        },
        "required": ["analysis_type", "document_path"]
      }
    }
  ]
}
```

### §6.6A Default output, citation packs, and freshness

**Default case output:** `["header", "synopsis", "holdings", "keycite"]`

This is the default because it gives the user:
- the citation and court/date context,
- the summary and holdings,
- the treatment signal,
without spending tokens on the full opinion unless needed.

**Opinion length default:** `5000` characters unless explicitly overridden.

**Structured citation pack output**
When Elnor researches cases for a project, the Westlaw skill should emit a light-weight citation pack at:
```text
~/.openclaw/workspace/cases/<matter>/citations.json
```

Example:
```json
{
  "project": "Henderson",
  "created": "2026-03-08T10:30:00Z",
  "citations": [
    {
      "case_name": "Tellabs, Inc. v. Makor Issues & Rights, Ltd.",
      "reporter_cite": "551 U.S. 308",
      "parallel_cites": ["127 S. Ct. 2499", "168 L. Ed. 2d 179"],
      "court": "Supreme Court of the United States",
      "date": "2007-06-21",
      "keycite_status": "positive",
      "keycite_flags": [],
      "westlaw_url": "https://1.next.westlaw.com/Document/...",
      "holdings_summary": "Court held that courts must consider competing inferences of scienter...",
      "relevance_note": "Key authority on scienter pleading standard under PSLRA"
    }
  ]
}
```

Citation packs are inputs to:
- Word drafting flows,
- Legal Tables prepopulation,
- matter-level research tracking.

**Freshness**
If the user asks for the current state of law, current rules, current deadlines, or other time-sensitive legal propositions, the final answer path should invoke DOC2 freshness verification before the answer is finalized.

### §6.7 Workflow Examples

**Example 1: Find and read a case**
```
User: "Elnor, pull up Tellabs v. Makor from the Supreme Court"
Agent:
  1. westlaw_check_session → "active"
  2. westlaw_search(query: "Tellabs v. Makor Issues & Rights", content_type: "cases")
     Internally: snapshot → find search textbox ref → type query → find Search button ref → click → wait → re-snapshot
  3. westlaw_read_results → sees "Tellabs, Inc. v. Makor Issues & Rights, Ltd. — 551 U.S. 308"
     Internally: snapshot → parse result title links → extract text
  4. westlaw_open_result(result_number: 1)
     Internally: snapshot → find result title link #1 ref → click → wait → guard check
  5. westlaw_read_case(sections: ["header", "synopsis", "holdings"])
     Internally: snapshot → extract case title → read synopsis text → read holdings
  6. Reports to user: case name, citation, holdings summary
```

**Example 2: Judge research for securities case**
```
User: "Elnor, I've got a securities class action assigned to Judge Tigar. What's his track record?"
Agent:
  1. westlaw_check_session → "active"
  2. westlaw_judge_search(judge_name: "Tigar")
     Internally: navigate to Analytics → snapshot → find search input ref →
     type "Tigar" → wait for auto-suggest → snapshot → find judge suggestion ref → click
  3. westlaw_judge_overview(case_type_filter: ["securities", "class_action"])
     Internally: snapshot → find case type filter → click securities checkbox →
     click apply → re-snapshot → extract ruling tendencies, speed data
  4. westlaw_judge_motions(case_type_filter: ["securities"], motion_type_filter: "Motion to Dismiss")
  5. westlaw_judge_outcomes(case_type_filter: ["securities"])
  6. Reports: grant rates for MTD in securities cases, time to rule, outcome breakdown
```

**Example 3: Know Your Judge — shadow DOM form**
```
User: "Elnor, ask Know Your Judge how Tigar handles scienter pleading under Tellabs"
Agent:
  1. westlaw_check_session → "active"
  2. westlaw_judge_search(judge_name: "Tigar") (if not already there)
  3. westlaw_know_your_judge(
       claims: "Securities fraud under Section 10(b) and Rule 10b-5...",
       facts: "Defendant company restated financials, insider sales...",
       specific_focus: "Scienter standard under Tellabs..."
     )
     Internally:
       - snapshot → find "Know your judge" tab ref → click → wait
       - evaluate: set claims textarea value through shadow DOM
       - evaluate: set facts textarea value through shadow DOM
       - evaluate: set focus textarea value through shadow DOM
       - snapshot → find Continue button → click (or evaluate click through shadow DOM)
       - wait for results → extract report text
  4. Reads and summarizes the analysis report
```

**Example 4: Session expired mid-task**
```text
Agent: westlaw_search(query: "securities fraud scienter")
Tool: snapshot shows auth.thomsonreuters.com login page
Agent:
  - if `allow_saved_profile_relogin = true` and no CAPTCHA appears:
    attempt one safe relogin using the managed browser's saved profile,
    then resume the search
  - otherwise:
    "Your Westlaw session has expired. Please log in again in the managed
     browser window and let me know when you're ready. I'll pick up where
     we left off."
```

**Example 5: Charge popup encountered**
```
Agent: westlaw_open_result(result_number: 3)
Guard: snapshot text contains "This document requires ancillary charges. Accept?"
       → find "Decline" or "Cancel" button ref → click → log
Agent: "Westlaw wanted to charge extra to access this document — 'ancillary charges.'
        I declined automatically. Want me to try a different approach, or should
        I go ahead with the charge?"
```

### §6.8 Shadow DOM Strategy

Westlaw uses SAF (Saffron) custom web components (`SAF-TEXT-AREA-V3`, `SAF-BUTTON-V3`, `SAF-SWITCH-V3`, `SAF-ANCHOR`) with shadow DOM. OpenClaw's snapshot system may not see inside these.

**Strategy (in priority order):**

1. **Try snapshot first** — Some shadow DOM elements expose accessible roles/labels that OpenClaw's snapshot can read. If the element appears in the snapshot with a usable ref, use it normally.

2. **Use `evaluate` for interaction** — When snapshot can't reach inside:
   ```javascript
   // Set value on a shadow DOM textarea
   const host = document.querySelector('[data-testid="claims-textarea"]');
   const textarea = host.shadowRoot.querySelector('textarea');
   textarea.value = "the claims text";
   textarea.dispatchEvent(new Event('input', { bubbles: true }));
   ```

3. **Text matching** — For SAF-ANCHOR links that have visible text, OpenClaw may find them by text content in snapshot even if the shadow DOM internals aren't visible.

4. **Screenshot + labels for debugging** — `openclaw browser snapshot --labels` overlays ref numbers on a screenshot, useful for identifying elements that don't appear clearly in text snapshots.

### §6.10 Implementation and ownership notes

**Runtime owner:** OpenClaw native browser tool.

**DOC3 owner responsibilities**
- Westlaw page knowledge
- charge guard rules
- research workflows
- capability manifests
- citation-pack format
- optional saved-profile relogin policy description

**Future connector note**
If Thomson Reuters / Westlaw API access is later obtained, implement it as a separate structured connector family (`westlaw_api.*`) and add it to the `surface_order` ahead of page-knowledge UI. Do not silently replace the browser skill contract.

**Suggested capability manifest example**
```json
{
  "capability_id": "westlaw.read_case",
  "family": "westlaw",
  "kind": "workflow_pattern",
  "title": "Read a Westlaw case",
  "origin_skill": "westlaw",
  "aliases": ["read westlaw case", "open case on westlaw"],
  "action_verbs": ["read", "open", "extract"],
  "surface_order": ["native_openclaw_browser", "page_knowledge_ui"],
  "runtime_binding": {
    "binding_kind": "workflow_pattern",
    "target": "westlaw::read_case"
  },
  "required_runtime_caps": ["browser_available", "westlaw_session"],
  "permissions": [],
  "requires_supervision": false,
  "dry_run_supported": false,
  "routing_eligible": true,
  "verification": [
    { "kind": "snapshot_label", "target": "case_title" },
    { "kind": "tool_result", "target": "keycite_status" }
  ],
  "health_probe": {
    "kind": "browser",
    "target": "snapshot",
    "timeout_ms": 3000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §6.9 Implementation Notes

**Snapshot token efficiency:** Use `--compact` and `--depth` flags to keep snapshot output manageable. Westlaw pages are heavy (many elements). `--interactive --compact` returns only actionable elements, reducing tokens significantly.

**Page load waits:** Westlaw pages are heavily dynamic. After every navigation:
- `openclaw browser wait --load networkidle --timeout-ms 15000`
- Then snapshot to confirm expected page loaded

**Reading large documents:** Case opinions can be very long. `westlaw_read_case` with `sections: ["opinion"]` should use `evaluate` to extract `#co_document` innerText in chunks, accumulating until the end of document or `max_length` is reached.

**Refs expire after every action.** This is the most important implementation detail. Every tool that does multiple actions (search: type query → click search → read results) must re-snapshot between each step. The page knowledge files help the agent quickly identify the right element in each fresh snapshot.

---
## §6A Setup & Permissions Checklist

One-time setup to grant OpenClaw / Terminal / host-app automation permissions on macOS. Without these, AppleScript wrappers or desktop automation fallbacks will fail with authorization errors.

### §6A.1 macOS Automation permissions

Grant the following in **System Settings → Privacy & Security → Automation**:

| Host app | Permission to control | Why |
|---|---|---|
| Terminal / iTerm / OpenClaw app host | Microsoft Outlook | Outlook wrappers |
| Terminal / iTerm / OpenClaw app host | Microsoft Word | Compare/open-in-app / legal tables macros |
| Terminal / iTerm / OpenClaw app host | System Events | UI fallback where needed |

If OpenClaw hosts execution directly, grant permissions to OpenClaw.app rather than Terminal.

### §6A.2 Accessibility permissions

Grant **Accessibility** only if mouse/keyboard fallback is needed for UI-only paths.

### §6A.3 Browser setup

**Managed browser (`openclaw` profile)**
- default for autonomous work,
- no extension attach step,
- preferred for Westlaw and other persistent-session sites.

**Extension relay (`chrome` profile)**
- only for an already-open real browser tab,
- user must explicitly attach the relay to the tab.

### §6A.4 Verification commands

```bash
# Managed browser
openclaw browser --browser-profile openclaw start
openclaw browser snapshot --interactive --compact

# Outlook
osascript ~/.openclaw/workspace/skills/outlook/scripts/email_list_accounts.applescript

# Word
osascript ~/.openclaw/workspace/skills/word/scripts/open_in_app.applescript '{"path": "/tmp/test.docx"}'
```

### §6A.5 First-run friction reduction

On first use, Elnor should:
1. verify Outlook access,
2. verify Word access,
3. verify managed-browser responsiveness,
4. surface precise remediation if permissions or sessions are missing.
## §7 Bitwig Skill (Phase 1)

### §7.1 Overview

Bitwig is the first non-legal, non-browser exemplar in DOC3. It exists to prove that Elnor’s capability-learning substrate generalizes beyond legal office tools.

Bitwig is intentionally hybrid:
- it has controller-extension / API surfaces,
- it has keyboard shortcuts,
- it has MIDI-triggerable actions,
- and it still has UI gaps that may require page-knowledge / desktop automation fallback.

That makes it the right proving ground for learning new capabilities safely.

**Path:** `~/.openclaw/workspace/skills/bitwig/`

### §7.1A Capability family and control surfaces

**Capability family:** `bitwig`

**Primary control surfaces (in order):**
1. `app_api`
2. `midi_binding`
3. `keyboard_shortcut`
4. `page_knowledge_ui`
5. `raw_ui`

**Required artifacts**
```text
skills/bitwig/SKILL.md
skills/bitwig/capabilities/transport_play.json
skills/bitwig/capabilities/track_select.json
skills/bitwig/capabilities/track_arm.json
skills/bitwig/capabilities/device_parameter_set.json
skills/bitwig/capabilities/scene_launch.json
skills/bitwig/capabilities/render_export.json
skills/bitwig/capabilities/shortcut_send.json
skills/bitwig/capabilities/midi_trigger.json
skills/bitwig/control-surface.json
skills/bitwig/shortcut-registry.json
skills/bitwig/midi-registry.json
skills/bitwig/ui-anchors/*.json
skills/bitwig/knowledge/*.md
```

### §7.2 Architecture

**Purpose.**
- Give Elnor a structured, semantic way to control Bitwig for transport, tracks, clips/scenes, device parameters, rendering, and other repeatable actions.
- Allow “teach mode” to capture useful shortcut or MIDI patterns and turn them into proposed capabilities.
- Minimize fragile cursor-based automation.

**Does not do.**
- Does not promise arbitrary plug-in UI automation in MVP.
- Does not treat raw clicking as the preferred route.
- Does not auto-install new controller extensions from casual traces without review.

### §7.3 Bitwig control-surface policy

**Normative preference order**
1. Use Bitwig controller/API or any existing structured bridge when available.
2. Use MIDI bindings for stable semantic actions (transport, scene launch, track arm, device macro triggering).
3. Use keyboard shortcuts when those are stable and semantically clear.
4. Use UI anchors / page knowledge only for unsupported gaps.
5. Use raw UI as last resort and record fallback traces.

**Examples**
- “Play transport” → API or shortcut, never raw click first.
- “Arm Bass track” → API / MIDI / shortcut before clicking a small button.
- “Set macro 3 to 65%” → API or mapped control surface if available.
- “Open an obscure plug-in menu item” → UI-anchor path if no semantic surface exists.

### §7.4 SKILL.md

```markdown
---
name: bitwig
description: Control Bitwig Studio with controller/API routes, MIDI bindings, keyboard shortcuts, and limited UI fallback. Transport, tracks, devices, scenes, rendering, production workflow.
allowed-tools: Bash(python3:*) Read
---

# Bitwig Integration

Use the structured routes in this skill before attempting any raw UI control.

## Preferred order
1. Bitwig controller/API route
2. MIDI binding
3. Keyboard shortcut
4. UI anchors
5. Raw UI only when nothing else works

## Typical tasks
- Start/stop playback → `bitwig.transport_play` / `bitwig.transport_stop`
- Select / arm track → `bitwig.track_select`, `bitwig.track_arm`
- Change a mapped device parameter → `bitwig.device_parameter_set`
- Trigger scene / clip launcher action → `bitwig.scene_launch`
- Render/export → `bitwig.render_export`

## Teach mode
If the user demonstrates a repeatable shortcut or MIDI-triggered action, capture it as a teach-session candidate. Do not silently install it as a permanent capability. Submit a proposal bundle for review.

## Critical rules
- Prefer semantic surfaces over UI.
- For render/export or destructive actions, verify the target project/state before acting.
- If fallback to UI is required, record the trace and any useful anchors.
```

### §7.5 Registries

**Shortcut registry**
```json
{
  "app": "bitwig",
  "shortcuts": [
    {
      "action_id": "transport.play_toggle",
      "label": "Play / Stop transport",
      "keys": ["SPACE"],
      "notes": "Default transport toggle"
    }
  ]
}
```

**MIDI registry**
```json
{
  "app": "bitwig",
  "bindings": [
    {
      "binding_id": "track.arm.bass",
      "label": "Arm Bass track",
      "transport": "midi",
      "message": {
        "type": "cc",
        "channel": 1,
        "controller": 12,
        "value": 127
      },
      "verify": "track_armed:bass"
    }
  ]
}
```

### §7.6 Capability manifests (examples)

```json
{
  "capability_id": "bitwig.track_arm",
  "family": "bitwig",
  "kind": "promoted_skill",
  "title": "Arm a Bitwig track",
  "origin_skill": "bitwig",
  "aliases": ["arm track", "record-enable track"],
  "action_verbs": ["arm", "select"],
  "surface_order": ["app_api", "midi_binding", "keyboard_shortcut", "page_knowledge_ui"],
  "runtime_binding": {
    "binding_kind": "workflow_pattern",
    "target": "bitwig::track_arm"
  },
  "required_runtime_caps": ["bitwig_running"],
  "permissions": [],
  "requires_supervision": false,
  "dry_run_supported": false,
  "routing_eligible": true,
  "verification": [
    { "kind": "api_state", "target": "track.armed", "expected": "true" }
  ],
  "health_probe": {
    "kind": "noop",
    "target": "bitwig",
    "timeout_ms": 1000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §7.7 Teach mode for Bitwig

Bitwig is a primary teach-mode target.

**Allowed teaching inputs**
- demonstrated keyboard shortcut sequence,
- demonstrated MIDI trigger,
- a repeated successful hybrid trace,
- a manually provided mapping file.

**Promotion flow**
1. capture trace,
2. attach relevant shortcut / MIDI / UI-anchor evidence,
3. create capability proposal,
4. review and approve,
5. materialize manifest + registry entry + tests,
6. run smoke test against Bitwig.

### §7.8 Acceptance expectations

- A stable shortcut or MIDI route can become a capability proposal.
- Fallback to UI is recorded as fallback, not treated as normal success.
- A promoted Bitwig capability becomes visible in the capability bridge and in Q capability awareness surfaces.

## §7A Microsoft 365 MCP Family

### §7A.1 Overview
The Microsoft 365 family is the first MCP-first connector family in DOC3. It complements — not replaces — the local Outlook and Word skills.

**Primary targets**
- Microsoft 365 Search
- SharePoint / OneDrive
- Outlook Mail
- Outlook Calendar
- Teams
- Word cloud content / comments

**Design rule**
- Treat Microsoft connectors as the preferred path for live cloud system-of-record access.
- Keep local Outlook / Word skills for desktop-specific or UI-specific work.
- All Microsoft connectors must be feature-flagged and health-checked because upstream availability may vary by tenant / rollout stage.

### §7A.2 Capability family and control surfaces

```json
{
  "family": "microsoft365",
  "default_surface_order": ["mcp_connector", "bridge_tool", "native_openclaw_browser", "raw_ui"]
}
```

**Representative capabilities**
- `m365.search`
- `m365.sharepoint_list_documents`
- `m365.onedrive_fetch_file`
- `m365.outlook_mail_search`
- `m365.calendar_check_availability`
- `m365.calendar_create_event`
- `m365.teams_post_message`
- `m365.word_fetch_content`

### §7A.3 Microsoft skill-pack support files

```text
skills/microsoft365/
├── SKILL.md
├── capabilities/
│   ├── m365.search.json
│   ├── m365.sharepoint_list_documents.json
│   ├── m365.onedrive_fetch_file.json
│   ├── m365.outlook_mail_search.json
│   ├── m365.calendar_create_event.json
│   ├── m365.word_fetch_content.json
│   ├── m365.teams_post_message.json
├── mcp-bindings.json
├── support-packs/
│   ├── microsoft-policies.json
│   ├── microsoft-naming-conventions.json
└── tests/
    ├── smoke-tests.json
```

**Binding schema**
```ts
import { z } from "zod";

export const MCPBindingSchema = z.object({
  capability_id: z.string().max(160),
  server_id: z.string().max(64),
  tool_name: z.string().max(120),
  ask_first: z.boolean().default(false),
  fallback_surface_order: z.array(ControlSurfaceSchema).default([]),
  schema_version: z.literal(1),
});
```

### §7A.4 Project / matter / work-object routing

When a project/matter is bound to OneDrive or SharePoint:
1. resolve project identity from DOC7/Core;
2. use Microsoft 365 Search or SharePoint/OneDrive connector first;
3. only fall back to local file search if the file is known to be synced locally and policy allows it;
4. browser relay is the final fallback.

### §7A.5 Example manifests

```json
{
  "capability_id": "m365.sharepoint_list_documents",
  "family": "microsoft365",
  "kind": "bridge_tool",
  "title": "List project documents from SharePoint",
  "origin_skill": "microsoft365",
  "aliases": ["list matter documents", "find project files", "show sharepoint docs"],
  "action_verbs": ["search", "list", "fetch"],
  "surface_order": ["mcp_connector", "bridge_tool", "native_openclaw_browser", "raw_ui"],
  "runtime_binding": {
    "binding_kind": "bridge_tool",
    "target": "m365.sharepoint.listDocuments"
  },
  "required_runtime_caps": ["mcp_connector:sharepoint-onedrive"],
  "permissions": ["m365:files.read"],
  "requires_supervision": false,
  "dry_run_supported": true,
  "routing_eligible": true,
  "verification": [
    { "kind": "tool_result", "target": "items.length", "expected": ">=1" }
  ],
  "health_probe": {
    "kind": "tool",
    "target": "m365.sharepoint.health",
    "timeout_ms": 3000
  },
  "learning_policy": {
    "teach_mode_allowed": true,
    "auto_capture_trace": true,
    "proposal_required_for_mutation": true
  },
  "schema_version": 1
}
```

### §7A.6 Connector settings / user control rules

The user must be able to:
- disable all Microsoft connectors,
- disable connector use for a specific provider/runtime,
- disable individual tools like Teams posting or Calendar writes,
- force ask-first for write actions,
- and view recent connector receipts.

### §7A.7 Acceptance expectations
- connector health failure must show as degraded/unavailable, not silent omission;
- policy deny must stop the route before tool execution;
- OneDrive/SharePoint routing must preserve project identity and provenance in receipts.

## §7B Acrobat / PDF Services Family

### §7B.1 Overview
Acrobat/PDF work is a strong custom-adapter target. Use a structured PDF service or custom adapter when available; otherwise fall back to local PDF tooling / browser / OpenClaw file paths.

**Representative capabilities**
- `acrobat.extract_text`
- `acrobat.extract_tables`
- `acrobat.ocr_pdf`
- `acrobat.create_redacted_copy`
- `acrobat.prepare_review_bundle`

### §7B.2 Control policy
Preferred order:
1. structured connector / adapter,
2. local deterministic PDF tooling,
3. browser/UI,
4. raw UI.

### §7B.3 Why this is separate from Word
PDF semantics and OCR/extraction/assembly pipelines should not be bolted into the Word family. They deserve their own connector family and tests.

## §7C Gmail / Google Family

### §7C.1 Overview
Gmail/Google support must coexist with existing native/OpenClaw email surfaces.

**Rule**
- If a strong native/OpenClaw path already exists for the current runtime, use it.
- If a Google connector or future MCP adapter is healthier / more expressive for the task, prefer that connector.

**Representative capabilities**
- `gmail.search_mail`
- `gmail.read_thread`
- `google.drive_search`
- `google.drive_fetch`

### §7C.2 Acceptance note
Gmail/Google connector behavior must not silently duplicate or conflict with any existing native mail/calendar capability already exposed in the active runtime.

## §7D External AI Runtime Targets

### §7D.1 Overview
OpenAI, Claude, Codex, and similar systems are generally **clients/runtimes** for MCP, not source-of-truth document systems. They should be modeled as:
- target runtimes that may consume ELNOR-managed MCP servers,
- or providers that may be allowed/denied access to selected MCP servers.

### §7D.2 Policy rules
- provider policy must allow “use this MCP server from OpenAI / Anthropic / Codex / OpenClaw” separately;
- telemetry must show which runtime actually used the connector;
- do not treat “OpenAI” or “Claude” as equivalent to a file store or project repository.


## §8 Excel and Other Apps Policy

### §8.1 Excel skill status

Excel remains a stub / future capability family, but it now follows the same capability-substrate rules as every other app family.

**Planned Excel capabilities**
- `excel.open_workbook`
- `excel.read_cells`
- `excel.write_cells`
- `excel.read_sheet_names`
- `excel.get_cell_formula`
- `excel.run_macro`
- `excel.save`
- `excel.export_pdf`

**Preferred control surfaces**
1. AppleScript / app API
2. structured wrapper
3. raw UI only when necessary

### §8.2 New app-family checklist

Before adding a new app family to DOC3, the spec must define:
1. native OpenClaw alternatives checked first,
2. bridge/API surface checked,
3. control-surface order,
4. capability manifests,
5. verification rules,
6. permissions / setup requirements,
7. teach-mode viability,
8. health probe,
9. acceptance tests.

### §8.3 Apps that usually do not need custom skills

These are typically well served by native OpenClaw tools, shell commands, or simple launch flows:
- Zoom
- Teams
- Finder
- Preview
- simple `open -a "App Name"` launches

### §8.4 Apps to check OpenClaw-native integrations for first

Before building custom skills, check native OpenClaw tools / bundled skills / gateway surfaces for:
- browser/CDP
- Gmail / mail integrations
- calendars
- Slack
- nodes / canvas / device surfaces

If the native integration already provides the runtime ability, DOC3 should add only the app-specific knowledge layer, not duplicate runtime control.
## §9 Acceptance Tests

### §9.1 Outlook tests

| # | Test | Validates |
|---|---|---|
| 1 | "Check my work email" → `outlook_email_search` returns structured results | email search |
| 2 | "Read the email from Jane Smith" → `outlook_email_read` returns body text | email body read |
| 3 | "What’s on my calendar this week?" → `outlook_calendar_read` returns events | calendar read |
| 4 | "Put Henderson MTD deadline on March 15 in Court Deadlines" → creates event autonomously and reports after | autonomous calendar create |
| 5 | "Reply to Jane about the discovery stip" to known correspondent → send autonomously and report after | autonomous send to known recipient |
| 6 | new external recipient → system asks for confirmation before send | confirmation gate |
| 7 | write disabled in `config.json` → structured error JSON returned | per-account write protection |
| 8 | "Save the attachments from Jane’s email to the Henderson folder" → `outlook_email_save_attachments` returns saved paths | attachment pipeline |
| 9 | broad search on large mailbox returns recent results quickly or asks to narrow query | search ladder |
| 10 | composite `message_id` from search still resolves for read/attachment-save later | stable message ID |

### §9.2 Word tests

| # | Test | Validates |
|---|---|---|
| 11 | create a doc from content and save to path | document creation |
| 12 | read structure of a brief | structure read |
| 13 | read tracked changes from opposing counsel markup | track changes read |
| 14 | read comments | comments read |
| 15 | edit a brief → working copy only, original untouched | never-touch-original |
| 16 | generate tracked version via `word_compare` after editing | compare workflow |
| 17 | every change in compare output can be Accepted/Rejected in Word | real tracked changes |
| 18 | `edit.py` rejects an original-path edit attempt | working-copy enforcement |
| 19 | attachment → save → working copy → edit → compare → PDF export | full pipeline |
| 20 | `word_accept_reject_changes` modifies the expected number of revisions | accept/reject path |

### §9.3 Legal Tables tests

| # | Test | Validates |
|---|---|---|
| 21 | generate TOC, then report summary and invite post-generation review | TOC autonomy |
| 22 | generate TOA, then report counts and uncertain citations | TOA autonomy |
| 23 | user corrects a citation category after generation → update applied and learning signal emitted | correction learning |
| 24 | standing orders / corrections loaded before legal-tables workflow | preflight context |
| 25 | style override (italic vs underline) persists across later tasks | preference persistence |
| 26 | v1 does not auto-mark ambiguous `id.`/`supra` short forms | conservative short-form rule |

### §9.4 Browser tests

| # | Test | Validates |
|---|---|---|
| 27 | browser skill identifies a registered site from `site-registry.json` | site check |
| 28 | browser skill uses managed `openclaw` profile by default for autonomous work | profile preference |
| 29 | extension relay detached → current-session availability becomes unsupported | honest runtime truth |
| 30 | charge guard catches visible overlay dialog but ignores page-body fee text | false-positive control |
| 31 | page knowledge drift produces a candidate/proposal rather than silent file mutation | self-healing discipline |

### §9.5 Westlaw tests

| # | Test | Validates |
|---|---|---|
| 32 | active managed session detected correctly | session check |
| 33 | expired session with no saved-profile relogin configured → user prompted to log in manually | safe relogin policy |
| 34 | expired session with saved-profile relogin enabled and normal login page → relogin attempted safely | optional relogin |
| 35 | CAPTCHA / unusual verification → user notified, no blind automation | safe failure |
| 36 | search + read results + open result + read case returns default sections only | default output narrowing |
| 37 | charge dialog overlay with ancillary-charge language is declined | charge guard true positive |
| 38 | opinion body mentioning fees/costs does not trigger the guard | charge guard false-positive prevention |
| 39 | citation pack file emitted for a research session | citation pack generation |
| 40 | current-law / current-rule answer path invokes freshness verification before final answer | DOC2 integration |

### §9.6 Capability bridge tests

| # | Test | Validates |
|---|---|---|
| 41 | adding a capability manifest produces a bridge entry after rebuild | bridge emission |
| 42 | bridge entry includes metadata_ref, health_status, and origin_owner | bridge completeness |
| 43 | installed capability but detached runtime surface → `session_availability = unsupported` | installed vs usable distinction |
| 44 | stale runtime registry surfaces degraded bridge state rather than fake availability | honest degradation |

### §9.7 Teach mode and promotion tests

| # | Test | Validates |
|---|---|---|
| 45 | Teach Elnor on a successful trace creates a `TeachSession` | teach-mode capture |
| 46 | teach session creates a proposal bundle, not a permanent manifest mutation | approval discipline |
| 47 | approved promotion writes manifest + metadata + tests and triggers bridge rebuild | promotion materialization |
| 48 | rejected proposal leaves canonical DOC3 artifacts unchanged | rejection safety |

### §9.8 Hybrid control tests

| # | Test | Validates |
|---|---|---|
| 49 | Bitwig transport uses shortcut/API before UI fallback | semantic preference |
| 50 | Word visual-only action can fall back to UI and records that fallback | fallback trace |
| 51 | future partial-API app chooses API for read, UI for irreducible gap, and verifies each step | hybrid step planning |

### §9.9 Health / quarantine / reprobe tests

| # | Test | Validates |
|---|---|---|
| 52 | repeated similar failures degrade a capability | health degradation |
| 53 | critical failure quarantines a capability | quarantine |
| 54 | successful reprobe restores healthy after threshold | reprobe recovery |
| 55 | quarantined capability is visible but not routable by default | safe routing |

### §9.10 Bitwig tests

| # | Test | Validates |
|---|---|---|
| 56 | teach a keyboard shortcut for transport → proposal created | teach-mode shortcut capture |
| 57 | teach a MIDI trigger for track arm → proposal created | teach-mode MIDI capture |
| 58 | promoted Bitwig capability appears in capability bridge and Q awareness | bridge visibility |
| 59 | UI fallback in Bitwig remains logged as fallback | trace quality |
| 60 | semantic route beats cursor route when both exist | control-surface preference |

### §9.11 Portable skill import tests
1. Import an Anthropic/OpenClaw-style skill bundle with valid frontmatter and required files → report is `compatible=true`, grade `direct`, and stage state becomes `staged`.
2. Import a bundle with vague description and no negative triggers → report contains warnings and trigger-test action items.
3. Import a bundle that references unavailable connectors → report is `requires_adapter=true` and stage state becomes `needs_adapter`.

### §9.12 MCP routing tests
1. Project bound to SharePoint/OneDrive + healthy connector + policy allow → route uses `mcp_connector` and emits a capability receipt.
2. Same project with connector disabled for provider → route falls back to local/OpenClaw path or returns “connector disabled” if no safe fallback exists.
3. Browser route must not be chosen when a healthy allowed structured connector exists for the same capability.

### §9.13 Provider policy / ask-first tests
1. Provider deny on `openai` for `sharepoint-onedrive` → execution stops before connector invocation and receipt shows `approval_state=denied`.
2. Ask-first write tool (Teams post / calendar write) → system prompts; deny leaves no side effects.
3. Global `Allow MCP = false` → all MCP routes become unavailable and capability awareness reflects that.

### §9.14 Project resolver tests
1. Request says “this matter” in an active project room → project resolver returns the correct OneDrive/SharePoint binding.
2. Same request with missing project binding → system degrades honestly and asks for clarification or falls back to local route only if a safe local binding exists.
3. Bucket alias and explicit project ID both present → explicit project ID wins.

### §9.15 Telemetry / receipt tests
1. Successful SharePoint fetch → receipt shows `route_surface=mcp_connector`, `connector_server_id`, provider/runtime, action class, and project ID.
2. Successful local Word Compare flow → receipt shows local surfaces, not MCP.
3. Connector auth broken → connector badge / health panel shows degraded state and remediation action.


## §10 Files to Create

### §10.1 Capability-learning substrate

| File / Path | Purpose |
|---|---|
| `skills/<skill>/capabilities/*.json` | machine-readable manifests |
| `skills/<skill>/control-surface.json` | per-family control-surface order and policies |
| `skills/<skill>/ui-anchors/*.json` | reusable UI anchors |
| `ELNOR_MEMORY/system/capabilities/bridge_entries_current.json` | canonical compact bridge export |
| `ELNOR_MEMORY/system/capabilities/metadata/<capability_id>.json` | rich metadata sidecars |
| `ELNOR_MEMORY/system/capabilities/health_current.json` | health/state view |
| `ELNOR_MEMORY/system/capabilities/stats_current.json` | capability stats |
| `ELNOR_MEMORY/system/capabilities/traces.jsonl` | execution traces |
| `ELNOR_MEMORY/system/capabilities/teaching_sessions/<id>.json` | teach-mode sessions |
| `apps/ec-service/src/capabilities/*` | bridge/trace/promotion modules |

### §10.2 Outlook

| File | Purpose |
|---|---|
| `skills/outlook/SKILL.md` | Outlook skill instructions |
| `skills/outlook/scripts/email_search.applescript` | email search |
| `skills/outlook/scripts/email_read.applescript` | read email body |
| `skills/outlook/scripts/email_draft.applescript` | draft email |
| `skills/outlook/scripts/email_send.applescript` | send email |
| `skills/outlook/scripts/email_list_accounts.applescript` | list accounts |
| `skills/outlook/scripts/email_save_attachments.applescript` | save attachments |
| `skills/outlook/scripts/calendar_read.applescript` | read events |
| `skills/outlook/scripts/calendar_create.applescript` | create event |
| `skills/outlook/scripts/calendar_update.applescript` | update event |
| `skills/outlook/scripts/calendar_delete.applescript` | delete event |
| `skills/outlook/scripts/calendar_list.applescript` | list calendars |
| `skills/outlook/config.json` | per-account write policy |
| `skills/outlook/capabilities/*.json` | Outlook capability manifests |
| `skills/outlook/control-surface.json` | Outlook control-surface policy |

### §10.3 Word

| File | Purpose |
|---|---|
| `skills/word/SKILL.md` | Word skill instructions |
| `skills/word/scripts/create.py` | create documents |
| `skills/word/scripts/read.py` | read document text |
| `skills/word/scripts/read_structure.py` | outline / headings |
| `skills/word/scripts/read_track_changes.py` | parse changes |
| `skills/word/scripts/read_comments.py` | parse comments |
| `skills/word/scripts/edit.py` | clean working-copy edits |
| `skills/word/scripts/insert_comment.py` | insert comments |
| `skills/word/scripts/find_replace.py` | find/replace |
| `skills/word/scripts/format.py` | formatting |
| `skills/word/scripts/style_create.py` | create styles |
| `skills/word/scripts/style_apply.py` | apply styles |
| `skills/word/scripts/headers_footers.py` | headers/footers |
| `skills/word/scripts/insert_section.py` | section breaks |
| `skills/word/scripts/insert_list.py` | lists |
| `skills/word/scripts/insert_signature_block.py` | signature blocks |
| `skills/word/scripts/highlight.py` | highlight |
| `skills/word/scripts/save_as.py` | save as |
| `skills/word/scripts/copy_content.py` | copy content |
| `skills/word/scripts/export_pdf.py` | export PDF |
| `skills/word/scripts/accept_reject_changes.py` | accept/reject revisions |
| `skills/word/scripts/open_in_app.applescript` | open in Word |
| `skills/word/scripts/compare.applescript` | Word Compare |
| `skills/word/scripts/toggle_track_changes.applescript` | toggle track changes |
| `skills/word/capabilities/*.json` | Word capability manifests |
| `skills/word/control-surface.json` | Word control-surface policy |

### §10.4 Legal Tables

| File | Purpose |
|---|---|
| `skills/legal-tables/SKILL.md` | Legal Tables instructions |
| `skills/legal-tables/scripts/toc_scan.py` | TOC scan |
| `skills/legal-tables/scripts/toc_preview.py` | TOC preview |
| `skills/legal-tables/scripts/toa_scan.py` | TOA structuring |
| `skills/legal-tables/scripts/toa_preview.py` | TOA preview |
| `skills/legal-tables/scripts/configure.py` | config get/set |
| `skills/legal-tables/scripts/run_macro.applescript` | macro bridge |
| `skills/legal-tables/capabilities/*.json` | Legal Tables manifests |
| `skills/legal-tables/control-surface.json` | Legal Tables control policy |
| `~/Library/.../Startup/Word/Elnor_Legal.dotm` | VBA macro template |

### §10.5 Browser / Westlaw

| File | Purpose |
|---|---|
| `skills/browser/SKILL.md` | Browser knowledge layer |
| `skills/browser/site-registry.json` | site registry |
| `skills/browser/scripts/guard.py` | guard logic |
| `skills/browser/page-knowledge/*.json` | browser generic page knowledge |
| `skills/browser/ui-anchors/*.json` | browser UI anchors |
| `skills/browser/capabilities/*.json` | browser manifests |
| `skills/westlaw/SKILL.md` | Westlaw skill instructions |
| `skills/westlaw/page-knowledge/login.json` | login page knowledge |
| `skills/westlaw/page-knowledge/home.json` | search/home page knowledge |
| `skills/westlaw/page-knowledge/search-results.json` | results page knowledge |
| `skills/westlaw/page-knowledge/case-reading.json` | case reading knowledge |
| `skills/westlaw/page-knowledge/litigation-analytics.json` | analytics knowledge |
| `skills/westlaw/scripts/guard_westlaw.py` | Westlaw guard logic |
| `skills/westlaw/config.json` | client ID, defaults, relogin policy |
| `skills/westlaw/capabilities/*.json` | Westlaw manifests |
| `skills/westlaw/control-surface.json` | Westlaw control policy |

### §10.6 Bitwig

| File | Purpose |
|---|---|
| `skills/bitwig/SKILL.md` | Bitwig instructions |
| `skills/bitwig/capabilities/*.json` | Bitwig manifests |
| `skills/bitwig/control-surface.json` | Bitwig control policy |
| `skills/bitwig/shortcut-registry.json` | keyboard shortcuts |
| `skills/bitwig/midi-registry.json` | MIDI bindings |
| `skills/bitwig/ui-anchors/*.json` | UI fallback anchors |
| `skills/bitwig/knowledge/*.md` | manuals / notes |

### §10.7 Skill Builder + portable packaging
```text
apps/ec-service/src/skills/parse-frontmatter.ts
apps/ec-service/src/skills/lint-portable-skill.ts
apps/ec-service/src/skills/grade-portability.ts
apps/ec-service/src/skills/import-scan.ts
apps/ec-service/src/skills/import-stage.ts
apps/ec-service/src/skills/trigger-tests.ts
apps/ec-service/src/skills/generate-from-teach.ts
apps/ec-service/src/skills/create-promotion-bundle.ts
apps/q-frontend/src/features/skills/import/SkillCompatibilityPanel.tsx
apps/q-frontend/src/features/skills/import/SkillImportWizard.tsx
```

### §10.8 MCP framework
```text
apps/ec-service/src/mcp/registry.ts
apps/ec-service/src/mcp/policy.ts
apps/ec-service/src/mcp/health.ts
apps/ec-service/src/mcp/resolve-route.ts
apps/ec-service/src/mcp/project-resolver.ts
apps/ec-service/src/mcp/emit-receipt.ts
apps/ec-service/src/mcp/providers/openai.ts
apps/ec-service/src/mcp/providers/anthropic.ts
apps/ec-service/src/mcp/providers/codex.ts
apps/ec-service/src/mcp/servers/knowledge-server.ts
apps/ec-service/src/mcp/servers/project-server.ts
apps/ec-service/src/mcp/servers/local-files-server.ts
apps/q-frontend/src/features/connectors/ConnectorSettingsPanel.tsx
apps/q-frontend/src/features/connectors/ConnectorHealthBadge.tsx
apps/q-frontend/src/features/connectors/MCPReceiptCard.tsx
ELNOR_MEMORY/system/mcp/registry_current.json
ELNOR_MEMORY/system/mcp/provider_policy_current.json
ELNOR_MEMORY/system/mcp/health_current.json
ELNOR_MEMORY/system/mcp/receipts.jsonl
```

### §10.9 Microsoft 365 / connector families
```text
skills/microsoft365/SKILL.md
skills/microsoft365/capabilities/m365.search.json
skills/microsoft365/capabilities/m365.sharepoint_list_documents.json
skills/microsoft365/capabilities/m365.onedrive_fetch_file.json
skills/microsoft365/capabilities/m365.outlook_mail_search.json
skills/microsoft365/capabilities/m365.calendar_create_event.json
skills/microsoft365/capabilities/m365.word_fetch_content.json
skills/microsoft365/capabilities/m365.teams_post_message.json
skills/microsoft365/mcp-bindings.json
skills/microsoft365/support-packs/microsoft-policies.json
skills/acrobat/SKILL.md
skills/gmail/SKILL.md
```

 `skills/bitwig/shortcut-registry.json` | keyboard shortcuts |
| `skills/bitwig/midi-registry.json` | MIDI bindings |
| `skills/bitwig/ui-anchors/*.json` | UI fallback anchors |
| `skills/bitwig/knowledge/*.md` | manuals / notes |


### §9.16 R9 autonomous skill mining tests
1. Repeated successful traces above threshold create a `skill_bundle_proposal`.
2. Ambiguous workflow in `ask_before_build` mode opens pre-build questions rather than building silently.
3. `build_then_review` mode drafts and tests a proposal before user review.
4. `auto_install_private` mode installs only into `experimental_private`, never `shared_promoted`.
5. Observation mode disabled => outside-ELNOR events do not create proposals.
6. Observation mode enabled => banner appears and events are captured as proposal inputs only.

### §9.17 R9 import / namespace / collision tests
1. Imported skill with colliding tool names is forced into `needs_adapter`.
2. Generated skill gets a runtime-safe namespace and cannot shadow native tools.
3. `depends_on` missing for a required support pack => staged proposal blocked.
4. Progressive disclosure keeps large schema docs out of hot-path `SKILL.md`.

### §9.18 R9 MCP operational tests
1. Healthy read-only connector routes as `allow` after install and auth.
2. Write action defaults to `ask_first`.
3. 429 response triggers backoff but does not immediately quarantine connector.
4. Connector health degrades after sustained failures and shows in Q.
5. Provider toggle “disable OpenAI MCP” blocks OpenAI-routed connector usage while preserving other providers.

### §9.19 R9 single-writer and projection tests
1. Wrapper emits `ConfigurationIntent` and cannot directly mutate canonical config.
2. EC writes canonical manifest then projects workspace copy atomically.
3. Bridge rebuild failure preserves last-good bridge and marks stale state.
4. Hooks/scripts attempting canonical writes are denied.

### §9.20 R9 partial deployment tests
1. Missing companion route surfaces explicit `partial_deployment` in Q.
2. Missing MCP policy backend disables destructive connector actions.
3. Missing skill-mining backend hides auto-build install actions while preserving manual Teach Elnor.

### §10.10 R9 shared contracts package
```text
packages/contracts/src/capabilities/bridge-entry.ts
packages/contracts/src/capabilities/control-surface.ts
packages/contracts/src/capabilities/hybrid-action-plan.ts
packages/contracts/src/capabilities/manifest.ts
packages/contracts/src/capabilities/metadata.ts
packages/contracts/src/capabilities/health.ts
packages/contracts/src/capabilities/trace.ts
packages/contracts/src/capabilities/teach.ts
packages/contracts/src/capabilities/receipts.ts
packages/contracts/src/mcp/auth-profile.ts
packages/contracts/src/mcp/server-registry.ts
packages/contracts/src/mcp/health.ts
packages/contracts/src/mcp/policy.ts
packages/contracts/src/mcp/route-decision.ts
packages/contracts/src/skills/frontmatter.ts
packages/contracts/src/skills/import-state.ts
packages/contracts/src/skills/proposal.ts
packages/contracts/src/skills/mining.ts
packages/contracts/src/projects/source-binding.ts
packages/contracts/src/common/error-envelope.ts
packages/contracts/src/common/configuration-intent.ts
packages/contracts/src/index.ts
```

### §10.11 R9 autonomous skill mining + review
```text
ELNOR_MEMORY/system/capabilities/mining_settings_current.json
ELNOR_MEMORY/system/capabilities/trace_clusters.jsonl
ELNOR_MEMORY/system/capabilities/skill_bundle_proposals.jsonl
ELNOR_MEMORY/system/capabilities/observed_actions.jsonl
ELNOR_MEMORY/system/capabilities/skill_proposal_feedback.jsonl
~/.openclaw/workspace/generated-skills/pending/
~/.openclaw/workspace/generated-skills/experimental_private/
```

### §10.12 R9 MCP policy + receipts + health
```text
ELNOR_MEMORY/system/mcp/servers_current.json
ELNOR_MEMORY/system/mcp/health_current.json
ELNOR_MEMORY/system/mcp/policy_current.json
ELNOR_MEMORY/system/mcp/auth_profiles_current.json
ELNOR_MEMORY/system/mcp/receipts.jsonl
ELNOR_MEMORY/system/mcp/backoff_state_current.json
```


---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 2 — Merged Addendum — DOC3 Additions for LlamaIndex Retrieval Sidecar R1


# DOC3 Additions for LlamaIndex Retrieval Sidecar (R1)

## Purpose

This document records the exact changes that should be folded into the **next revision of DOC3** to support the LlamaIndex sidecar defined in DOC18.

It is intentionally narrow:
- it does **not** redefine DOC18,
- it does **not** replace existing search/routing language,
- it gives the later DOC3 drafting pass a precise list of normative additions.

---

## Design decision

DOC3 should treat LlamaIndex as:

- an **optional retrieval provider**,
- a **sidecar semantic index**,
- a **search route** that ELNOR may choose,
- and **not** a replacement for:
  - OpenClaw local runtime/file access,
  - Microsoft live APIs / MCP routes,
  - ELNOR Core canonical memory,
  - DOC7 project/matter identity,
  - or QMD/internal search.

---

## Additions to include in the next DOC3 revision

## 1. Add a new search-provider kind

Add `llamaindex_index` to the canonical search-provider / retrieval-provider enum.

### Suggested contract text

```ts
export const SearchProviderKindSchema = z.enum([
  "m365_drive_search",
  "m365_graph_search",
  "m365_retrieval_api",
  "qmd_local_semantic",
  "llamaindex_index",
  "openclaw_local_fs",
  "browser_fallback",
]);
```

### Normative rule
`llamaindex_index` is:
- eligible only when corpus bindings and sidecar health allow it,
- preferred primarily for semantic or cross-corpus retrieval,
- not the default first choice for exact known-file lookup when a live Microsoft folder-scoped route is available.

---

## 2. Add a search-provider architecture note

DOC3 should add a normative subsection stating:

- ELNOR supports both **live retrieval providers** and **indexed retrieval providers**
- LlamaIndex is a **sidecar indexed provider**
- Microsoft Graph / Drive / Retrieval APIs are **live providers**
- OpenClaw/local filesystem is a **local direct-access provider**
- QMD is an **internal managed retrieval provider**

### Required doctrine
1. Prefer **live Microsoft routes** for exact and live source-of-truth lookups.
2. Prefer **LlamaIndex** for selected semantic corpora and fallback semantic search.
3. Prefer **QMD/internal search** for internal ELNOR knowledge and managed non-sidecar corpora.
4. Prefer **browser automation last**.

---

## 3. Add corpus binding references

DOC3 should add the concept of **retrieval corpus bindings** for search providers that operate over indexed corpora.

### Suggested schema snippet

```ts
export const RetrievalCorpusBindingRefSchema = z.object({
  corpus_id: z.string().max(120),
  provider_kind: z.enum(["llamaindex_index", "qmd_local_semantic"]),
  project_id: z.string().max(120).optional(),
  matter_id: z.string().max(120).optional(),
  source_binding_ref: z.string().max(240),
  enabled: z.boolean().default(true),
  schema_version: z.literal(1),
});
```

### Normative rule
Project/matter identity still comes from Core/DOC7.  
Corpus bindings only point to indexed corpora associated with that identity.

---

## 4. Add LlamaIndex routing rules

DOC3 should add route guidance like the following.

### Exact document lookup
For:
- “Find the motion to dismiss”
- “Find the latest declaration”
- “Find the complaint in Johnson”

Preferred order:
1. `m365_drive_search`
2. `m365_graph_search`
3. `openclaw_local_fs`
4. `llamaindex_index` only if no better exact route exists and the corpus is known
5. `browser_fallback`

### Semantic issue / argument retrieval
For:
- “Find anything in the litigation file arguing loss causation”
- “Find briefs in this folder that make this argument”
- “Find work product discussing this issue”

Preferred order:
1. `m365_retrieval_api`
2. `llamaindex_index`
3. `qmd_local_semantic`
4. `openclaw_local_fs`
5. `browser_fallback`

### Cross-corpus semantic retrieval
For:
- “Find similar arguments from the matter folder and brief bank”
- “Search selected work product and current matter together”

Preferred order:
1. `llamaindex_index`
2. `qmd_local_semantic`
3. `m365_retrieval_api` only where corpus binding and licensing make sense
4. `browser_fallback`

---

## 5. Add route scoring inputs for LlamaIndex

DOC3 should explicitly say that the Search Orchestrator scores `llamaindex_index` on:

- corpus binding match
- project/matter scope match
- provider health
- corpus freshness / staleness
- query class fit
- user/provider policy
- prior success/failure history

### Suggested reason codes
Add reason codes such as:
- `llamaindex_corpus_match`
- `llamaindex_cross_corpus_query`
- `llamaindex_stale_penalty`
- `llamaindex_health_degraded`
- `llamaindex_semantic_best_fit`

---

## 6. Add bounded adaptive learning for LlamaIndex routing

DOC3 should explicitly allow ELNOR to learn when LlamaIndex is the better route, but in a bounded way.

### Normative rule
ELNOR may update route scores using:
- accepted result
- rejected result
- zero hits
- stale corpus
- timeout
- reformulated query
- manual route override by user

But ELNOR may **not**:
- override disabled routes,
- ignore health/policy,
- or permanently lock a route without a reversible setting.

### Suggested setting fields
```ts
export const SearchRoutingPreferencesSchema = z.object({
  adaptive_routing_enabled: z.boolean().default(true),
  ask_before_route_preference_change: z.boolean().default(false),
  allow_project_scoped_route_pinning: z.boolean().default(true),
  allow_private_route_learning: z.boolean().default(true),
  schema_version: z.literal(1),
});
```

---

## 7. Add explicit non-goals for LlamaIndex

DOC3 should say plainly:

LlamaIndex is **not**:
- canonical memory,
- project/matter resolver,
- permission truth,
- live Microsoft metadata truth,
- or a replacement for OpenClaw local runtime abilities.

This matters because coding agents will otherwise happily turn every shiny framework into the emperor of the universe.

---

## 8. Add user controls and telemetry expectations

DOC3 should add a small normative UI section requiring that Q support:

### Provider controls
- enable/disable LlamaIndex provider
- view health
- view corpora
- refresh corpora
- disable LlamaIndex for a project/matter
- pin/prefer LlamaIndex for a project/matter if the user wants

### Search receipts
Search receipts should show:
- route used
- provider kind
- corpus ids
- freshness state
- project/matter scope
- reason for route choice

### Debug controls
In advanced mode:
- compare route
- “why this route?”
- stale warning
- health/degraded explanation

---

## 9. Add capability/health truth requirements

DOC3 should say that `llamaindex_index` is only considered `usable_now` when:

- sidecar health is healthy/degraded (not disabled or unknown),
- corpus binding exists,
- corpus is enabled,
- stale policy does not forbid use,
- provider policy allows it.

If any of those fail, the route should degrade cleanly and ELNOR should choose another provider.

---

## 10. Add corpus categories to support-pack / project-aware workflows

DOC3 should allow skill/capability manifests to hint at relevant semantic corpora.

### Example
A skill or capability may include:
- `support_packs`
- `connector_hints`
- `retrieval_corpus_hints`

```ts
export const RetrievalCorpusHintSchema = z.object({
  corpus_id: z.string().max(120),
  purpose: z.enum([
    "matter_semantic_search",
    "brief_bank_lookup",
    "prelit_lookup",
    "cross_corpus_support",
  ]),
  priority: z.enum(["critical", "useful", "background"]).default("useful"),
});
```

This lets a DOC3 skill say:
- “For this litigation argument search task, `johnson_matter` and `brief_bank` are useful corpora.”

---

## 11. Add companion-doc references that the next DOC3 should mention

The next DOC3 revision should include a short cross-doc note that:

- DOC18 defines the sidecar implementation
- DOC7 owns project/matter context and support-pack relationships
- DOC2 owns freshness/staleness semantics surfaced to the user
- DOC10 consumes route/capability truth
- DOC11 owns usable-now/runtime truth
- DOC16 may later carry deferred additions, but LlamaIndex itself is specified in DOC18

---

## 12. Suggested normative text to drop into DOC3

Use or adapt the following paragraph in the next DOC3 revision:

> **LlamaIndex Sidecar Retrieval Provider**
> ELNOR supports an optional `llamaindex_index` retrieval provider for selected semantic corpora. This provider is sidecar-based, corpus-scoped, and non-canonical: it does not replace ELNOR Core memory, project resolution, OpenClaw-native runtime/file access, or Microsoft live APIs. `llamaindex_index` is intended for semantic retrieval over selected corpora such as matter folders, brief banks, pre-litigation banks, and mixed work-product corpora. ELNOR may route to `llamaindex_index` when semantic or cross-corpus retrieval is a better fit than a live API route, subject to health, freshness, policy, and project/corpus bindings.

And:

> ELNOR must not treat `llamaindex_index` as source-of-truth identity or permission truth. Project/matter identity remains Core/DOC7-owned; live file permission/state truth remains owned by the underlying source system and associated live connectors. Search receipts and advanced debug views must show when `llamaindex_index` was used, which corpora were consulted, whether the corpus was stale, and why the route was selected.

---

## 13. Acceptance criteria for the future DOC3 revision

The next DOC3 revision should be considered complete on this topic only when:

1. `llamaindex_index` appears as a first-class search provider kind.
2. Route-preference rules explain when it outranks Microsoft live routes and when it does not.
3. Corpus bindings are represented as configuration/read-model inputs.
4. ELNOR adaptive routing rules can learn from outcomes without overriding policy.
5. UI and telemetry expectations for LlamaIndex are explicitly present.
6. Non-goals are stated clearly enough that coding agents cannot reinterpret LlamaIndex as canonical memory or project identity.
7. DOC18 is referenced as the implementation spec.

---

## Recommended decision for now

Do **not** try to implement LlamaIndex only by patching DOC3 prose.
Use DOC18 for the subsystem build and this document as the DOC3 integration checklist.


---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 3 — Merged Revision — DOC3 App Skills R9.1 (Retrieval Provider and Graph/Topology Alignment)



# DOC3 App Skills R9.1
## Retrieval Provider and Graph/Topology Alignment Revision

**Date:** 2026-03-10  
**Status:** canonical targeted revision draft — retrieval-lane truth, LlamaIndex routing, corpus bindings, and graph/topology boundary alignment  
**Supersedes:** DOC3 App Skills R9  
**Scope rule:** This is a focused R9.1 revision against R9. Unchanged sections of R9 remain operative unless explicitly replaced or extended below.

---

## What changed in R9.1

R9 already established capability packaging, MCP control surfaces, project-aware cloud routing, receipts, health, and hybrid local-vs-cloud control policy. R9.1 adds the missing retrieval-owner rules so semantic retrieval does not drift across docs:

- introduces `llamaindex_index` as a first-class retrieval provider kind,
- formalizes **retrieval lanes** so exact/live lookup, semantic corpus search, canonical memory search, and native runtime/local search stay distinct,
- adds **retrieval corpus bindings** and `retrieval_corpus_hints`,
- adds a canonical **retrieval provider receipt** contract for route traces and Q receipts,
- adds route scoring and bounded adaptive learning rules for semantic corpus retrieval,
- adds explicit non-goals so LlamaIndex does not become canonical memory or project identity,
- adds graph/topology boundary language so DOC3 can route to graph-aware consumers without owning graph truth.

---

## 1. Architecture additions

### 1.1V Retrieval-lane doctrine

ELNOR supports multiple retrieval lanes. These are not interchangeable.

```ts
// packages/contracts/src/retrieval/provider-receipts.ts
import { z } from "zod";

export const RetrievalLaneSchema = z.enum([
  "exact_live_lookup",
  "semantic_corpus",
  "canonical_memory",
  "native_runtime_local",
  "browser_fallback",
]);
```

**Normative doctrine**

1. `exact_live_lookup`  
   Preferred for exact file lookup, live metadata truth, live permissions truth, and source-of-record state.

2. `semantic_corpus`  
   Preferred for semantic issue search, cross-corpus search, selected matter/work-product corpora, and fallback semantic retrieval.

3. `canonical_memory`  
   Preferred for ELNOR-owned memory artifacts such as standing orders, corrections, preferences, historical decisions, and future CIL/MemorySearchService retrieval.

4. `native_runtime_local`  
   Preferred for OpenClaw-native local runtime/workspace memory and direct local path access.

5. `browser_fallback`  
   Use only when stronger semantic or structured surfaces are unavailable.

**Rule:** DOC3 owns routing doctrine and provider/lane truth for retrieval providers. It does not own canonical memory semantics or graph truth.

### 1.1W Retrieval provider kinds

Add `llamaindex_index` to the canonical search/retrieval provider enum.

```ts
export const SearchProviderKindSchema = z.enum([
  "m365_drive_search",
  "m365_graph_search",
  "m365_retrieval_api",
  "qmd_local_semantic",
  "llamaindex_index",
  "openclaw_local_fs",
  "browser_fallback",
]);
```

### 1.1X Retrieval provider receipt contract

DOC3 must treat retrieval receipts as first-class sibling contracts to connector receipts.

```ts
export const RetrievalProviderReceiptSchema = z.object({
  receipt_id: z.string().uuid(),
  trace_id: z.string().max(120).optional(),
  provider_kind: SearchProviderKindSchema,
  search_lane: RetrievalLaneSchema,
  corpus_ids: z.array(z.string().max(120)).default([]),
  project_id: z.string().max(120).optional(),
  matter_id: z.string().max(120).optional(),
  query_class: z.enum([
    "exact_document_lookup",
    "semantic_issue_lookup",
    "cross_corpus_lookup",
    "support_pack_lookup",
    "mixed_unknown",
  ]),
  route_reason_codes: z.array(z.string().max(120)).default([]),
  freshness_state: z.enum(["fresh", "stale", "unknown"]).default("unknown"),
  degraded_reason: z.string().max(240).optional(),
  latency_ms: z.number().int().nonnegative().optional(),
  result_count: z.number().int().nonnegative().default(0),
  user_override_applied: z.boolean().default(false),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Rule:** every retrieval route that influences a user-visible result or document recommendation must be able to produce this receipt shape or a compatibility mapping into it.

---

## 2. LlamaIndex provider role

### 2.1 LlamaIndex is a provider, not a memory system

DOC3 should state plainly:

`llamaindex_index` is:
- an optional retrieval provider,
- a sidecar indexed semantic provider,
- corpus-scoped,
- non-canonical.

It is **not**:
- canonical memory,
- project/matter identity,
- live permission truth,
- live Microsoft metadata truth,
- OpenClaw-native local/runtime truth,
- the broader graph/topology owner.

### 2.2 Retrieval architecture note

DOC3 should explicitly recognize both **live retrieval providers** and **indexed retrieval providers**.

- Microsoft Graph / Drive / Retrieval API = live providers
- `llamaindex_index` = indexed semantic corpus provider
- `qmd_local_semantic` = internal managed retrieval provider
- `openclaw_local_fs` = local direct-access provider

### 2.3 Route preference doctrine

**Exact known-file lookup**
1. `m365_drive_search`
2. `m365_graph_search`
3. `openclaw_local_fs`
4. `llamaindex_index` only if no better exact route exists and the corpus is known
5. `browser_fallback`

**Semantic issue / argument retrieval**
1. `m365_retrieval_api`
2. `llamaindex_index`
3. `qmd_local_semantic`
4. `openclaw_local_fs`
5. `browser_fallback`

**Cross-corpus semantic retrieval**
1. `llamaindex_index`
2. `qmd_local_semantic`
3. `m365_retrieval_api` where licensing and corpus binding make sense
4. `browser_fallback`

**Canonical ELNOR memory retrieval**
1. EC-owned memory search / future `MemorySearchService`
2. compatible internal/local fallback
3. never route canonical memory search to `llamaindex_index` as if it were the owner

---

## 3. Corpus bindings and hints

### 3.1 Retrieval corpus binding refs

DOC3 must add corpus binding references for indexed providers.

```ts
export const RetrievalCorpusBindingRefSchema = z.object({
  corpus_id: z.string().max(120),
  provider_kind: z.enum(["llamaindex_index", "qmd_local_semantic"]),
  project_id: z.string().max(120).optional(),
  matter_id: z.string().max(120).optional(),
  source_binding_ref: z.string().max(240),
  enabled: z.boolean().default(true),
  freshness_state: z.enum(["fresh", "stale", "unknown"]).default("unknown"),
  schema_version: z.literal(1),
});
```

**Rule:** Project/matter identity remains Core/DOC7-owned. Corpus bindings only attach indexed corpora to that identity.

### 3.2 Retrieval corpus hints on capability manifests

Add `retrieval_corpus_hints` to capability / skill metadata when the capability naturally benefits from semantic corpora.

```ts
export const RetrievalCorpusHintSchema = z.object({
  corpus_id: z.string().max(120),
  purpose: z.enum([
    "matter_semantic_search",
    "brief_bank_lookup",
    "prelit_lookup",
    "cross_corpus_support",
    "support_pack_lookup",
  ]),
  priority: z.enum(["critical", "useful", "background"]).default("useful"),
});

export const CapabilityManifestSchema = z.object({
  // existing R9 fields...
  retrieval_corpus_hints: z.array(RetrievalCorpusHintSchema).default([]),
});
```

**Rule:** Hints may influence route scoring and support-pack assembly, but they never override policy or health.

---

## 4. Route scoring and bounded adaptive learning

### 4.1 Route scoring inputs for `llamaindex_index`

DOC3 should explicitly score `llamaindex_index` on:

- corpus binding match,
- project/matter scope match,
- query class fit,
- provider health,
- corpus freshness/staleness,
- user/provider policy,
- prior success/failure history,
- support-pack relevance,
- topology-relevant metadata presence.

Suggested reason codes:

- `llamaindex_corpus_match`
- `llamaindex_cross_corpus_query`
- `llamaindex_semantic_best_fit`
- `llamaindex_hybrid_best_fit`
- `llamaindex_stale_penalty`
- `llamaindex_health_degraded`
- `llamaindex_topology_metadata_present`
- `llamaindex_support_pack_relevant`

### 4.2 Adaptive routing bounds

```ts
export const SearchRoutingPreferencesSchema = z.object({
  adaptive_routing_enabled: z.boolean().default(true),
  ask_before_route_preference_change: z.boolean().default(false),
  allow_project_scoped_route_pinning: z.boolean().default(true),
  allow_private_route_learning: z.boolean().default(true),
  schema_version: z.literal(1),
});
```

ELNOR may update route scores using:
- accepted result,
- rejected result,
- zero hits,
- stale corpus,
- timeout,
- reformulated query,
- manual route override by user.

ELNOR may **not**:
- override disabled routes,
- ignore health/policy,
- permanently lock a route without a reversible setting,
- silently collapse lane boundaries.

### 4.3 Usable-now rule

`llamaindex_index` is only `usable_now` when:
- sidecar health is healthy/degraded,
- a corpus binding exists,
- the corpus is enabled,
- stale policy does not forbid use,
- provider policy allows it.

If any of those fail, DOC3 must degrade cleanly to another provider.

---

## 5. Retrieval receipts, Q surfaces, and user controls

### 5.1 Receipts

Retrieval receipts are required for:
- provider route auditing,
- “why this route?” explanations,
- CIL/advisor explanation payloads,
- advanced search debugging,
- project/matter route pinning decisions.

### 5.2 Q surface expectations

Q must support:

**Provider controls**
- enable/disable provider
- view provider health
- view corpora
- refresh corpora
- disable provider for a project/matter
- pin/prefer provider for a project/matter

**Search receipts**
Show:
- route used
- provider kind
- lane
- corpus ids
- freshness state
- project/matter scope
- reason for route choice
- degraded reason if any

**Advanced mode**
- compare route
- “why this route?”
- stale warning
- health/degraded explanation
- corpus health snapshot used at decision time

### 5.3 Receipt chips and detail panels

R9 already requires connector receipt chips. R9.1 extends that expectation to retrieval receipts for search/document discovery flows.

Suggested chip examples:
- `Used LlamaIndex`
- `Used Microsoft Retrieval`
- `Used QMD Semantic`
- `Used Local Runtime Search`

Expandable detail should include:
- lane,
- corpora,
- freshness,
- degraded reason,
- reason codes,
- trace correlation.

---

## 6. Graph/topology boundary note

### 6.1 What DOC3 may do

DOC3 may:
- route to providers that supply topology-relevant metadata,
- pass graph/topology-friendly metadata downstream,
- preserve result provenance in receipts,
- allow route scoring to notice topology-relevant metadata presence,
- surface route/provider truth to Q and advisors.

### 6.2 What DOC3 does not own

DOC3 does **not** own:
- the broader document/claim/matter graph substrate,
- contradiction/supersession truth,
- topology snapshots,
- graph-neighbor walk policy,
- canonical memory relationship truth.

Those belong to other owner docs/read-models.

### 6.3 One-context-authority rule preserved

Graph/topology use must not cause DOC3 to bypass DOC10’s one-context-authority / bounded packaging discipline. Retrieval improvements may increase recall and explanation quality; they do not grant unlimited injection entitlement.

---

## 7. Required code changes

### 7.1 Contracts

```text
packages/contracts/src/retrieval/provider-receipts.ts
packages/contracts/src/retrieval/search-provider-kinds.ts
packages/contracts/src/retrieval/corpus-bindings.ts
packages/contracts/src/retrieval/preferences.ts
```

### 7.2 EC / orchestration-facing modules

```text
apps/ec-service/src/retrieval/route-scoring.ts
apps/ec-service/src/retrieval/receipt/write-retrieval-receipt.ts
apps/ec-service/src/retrieval/providers/llamaindex-client.ts
apps/ec-service/src/retrieval/providers/provider-health.ts
apps/ec-service/src/retrieval/corpora/current-views.ts
```

### 7.3 Q surfaces

```text
apps/q-dashboard/src/features/search/receipts/ReceiptChip.tsx
apps/q-dashboard/src/features/search/receipts/RetrievalReceiptPanel.tsx
apps/q-dashboard/src/features/settings/retrieval/ProviderList.tsx
apps/q-dashboard/src/features/settings/retrieval/CorpusList.tsx
```

---

## 8. Cross-doc references DOC3 must now mention

The next DOC3 revision must include a concise owner note:

- **DOC18** defines the sidecar implementation and corpus/provider truth for `llamaindex_index`.
- **DOC7** owns project/matter context and support-pack relationships that may influence corpus hints.
- **DOC10** owns route traces, authoritative context packaging, and suite-wide retrieval explanation surfaces.
- **DOC11** owns runtime truth for native/local runtime paths and must not be conflated with EC canonical memory search.
- **DOC15** may consume retrieval receipts and corpus hints for document suggestions/explanations, but does not own provider truth.
- **DOC16** preserves the broader graph/topology architecture while owner docs absorb the right parts incrementally.

---

## 9. Acceptance criteria

DOC3 R9.1 is complete on this topic only when:

1. `llamaindex_index` appears as a first-class search provider kind.
2. Retrieval lanes are named explicitly and used consistently.
3. Route-preference rules explain when `llamaindex_index` outranks live routes and when it does not.
4. Corpus bindings and `retrieval_corpus_hints` are represented canonically.
5. Retrieval receipts are specified clearly enough that coding agents can implement provider/lane receipts without guessing.
6. Adaptive routing is bounded by health, policy, and reversibility.
7. Non-goals are stated clearly enough that coding agents cannot reinterpret LlamaIndex as canonical memory or project identity.
8. Graph/topology boundary text prevents DOC3 from drifting into graph ownership.

---

## 10. Recommended decision for Wave B

Do not rewrite DOC3 around LlamaIndex.

R9.1 should absorb exactly the routing/provider-truth pieces needed to:
- recognize `llamaindex_index`,
- score it appropriately,
- show it honestly,
- keep it bounded,
- and hand coherent receipts to the rest of the suite.

That gives DOC3 the right ownership without turning it into DOC18 or the broader graph/topology spec.


---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 2 — Merged Addendum — Actions & Abilities / Learn IA and UX

# DOC3 Addendum Proposal R1 — Actions & Abilities, Capability Learning UX, Workflow Graphs, Categories, and Skill/Task Cohesion

**Status:** proposal for the next DOC3 revision  
**Purpose:** preserve and formalize the substantive DOC3 additions developed in the discussion branch after R9, especially:
- clearer Actions / Abilities / Learn information architecture
- category model (overlapping, color-coded, user-defined groups)
- clearer naming for learning actions
- internal workflow graph model shared by tasks and skills
- internal step taxonomy that is mostly hidden from users
- skill bundles as grouping/distribution artifacts
- stronger teach/observe/explore capability-learning UX
- micro-skill chaining and composite skills
- native OpenClaw reasoning working together with skill guidance

This proposal is additive to `DOC3 App Skills R9`.

## 1. Executive position

The next revision should be simple for the user and structured for ELNOR/runtime.

### User-facing concepts
- Actions
- Abilities
- Learn
- Categories

### Internal/spec/runtime concepts
- task
- task_template
- automation
- skill
- connector
- bundle
- capability
- workflow_graph
- step_type
- proposal
- experimental_install

The user should mostly interact through plain-language objects and a unified UI.

## 2. Core naming and information architecture decisions

### 2.1 Top-level user-facing area
Use the top-level area name:

# Actions & Abilities

This should replace terms like Library or Automation Studio.

### 2.2 Primary user-facing browser groups

Within Actions & Abilities, the browser/second column should have three root groups:

#### Actions
Subcategories:
- Active
- Tasks
- Automations

#### Abilities
Subcategories:
- Skills
- Connectors
- Learn

#### Categories
User-defined, overlapping, color-coded groupings. Not object types.

### 2.3 Clarification of “Categories”
User-facing term: Category  
Internal/spec synonym: Collection

A category behaves like Ableton-style collections:
- any item can belong to multiple categories
- categories overlap
- categories may be broad or extremely specific
- categories are color-coded and user-defined
- categories are browser/filter overlays, not ownership containers

Eligible items:
- tasks
- task templates
- automations
- skills
- bundles
- learning proposals
- experimental/generated skills
- optionally connectors

Required UX:
Right-click menu item:
- Add to Category…

This opens a multi-select picker of existing color-coded categories, with quick-create support for a new category.

## 3. Object model: what the user sees vs what the system uses

### 3.1 Actions
Actions are things ELNOR is doing, can do now, or can schedule/run.

```ts
export const ActionObjectKindSchema = z.enum([
  "task_instance",
  "task_template",
  "automation_job",
  "automation_template",
  "action_history_entry",
]);
```

Task remains the specific coded object already defined in your system.

### 3.2 Abilities
Abilities are reusable things ELNOR can use to perform actions.

```ts
export const AbilityObjectKindSchema = z.enum([
  "skill",
  "bundle",
  "connector",
  "generated_skill_proposal",
  "experimental_skill",
]);
```

Meanings:
- Skill = reusable behavior/workflow/capability template
- Connector = access path to an external or structured system/service
- Bundle = grouping/distribution artifact for related skills/support material
- Learn = UI/process area for creating or improving abilities

### 3.3 Why “Capability” should stay mostly internal
Keep Capability as an internal/runtime term.
Use Abilities as the broader user-facing umbrella.
Represent learned/taught capabilities mostly as Skills in the UI once accepted.

## 4. Browser / main pane / right pane layout

### 4.1 Layout model
- Leftmost menu rail: Queue/Chat, Actions & Abilities, Rooms, Research/Docs, Settings
- Second column = Browser
- Main center pane = primary work surface
- Right pane = Chat/Queue/live interaction

### 4.2 Browser behavior
If the user clicks:
- Actions -> browser focuses to action categories; main pane opens unified Actions landing page
- Tasks -> browser focuses to task organization; main pane opens Tasks page
- Automations -> browser focuses to automation organization; main pane opens Automations page
- Abilities -> browser focuses to abilities; main pane opens unified Abilities landing page
- Skills -> browser focuses to skills organization; main pane opens Skills page
- Connectors -> browser focuses to connector organization; main pane opens Connectors page
- Learn -> browser focuses to learning/review sessions; main pane opens Learn page

The browser is for organization and quick selection. The main pane is the primary place where work is viewed and managed.

### 4.3 Search and filter behavior
The browser should have:
- search bar
- type filters
- status filters
- source filters
- category filters
- sort controls

The main pane should also support:
- tabs
- filter chips
- sorting
- drawers
- multi-select actions

## 5. Detailed user-facing IA

### 5.1 Actions
#### Active
Show currently running tasks, in-flight action plans, automations firing/running, and recent blocked/error actions.

#### Tasks
Task details belong primarily in Q Core / EC Core task specs, but DOC3 should note:
- tasks are structured action objects
- they may be multi-step
- they may use skills/connectors internally
- they support saved templates and queue integration

#### Automations
For:
- cron-like recurring actions
- heartbeat-generated recurring behaviors
- nightly updates
- scheduled recurring checks

#### Action landing page
If user clicks Actions root:
Show a unified landing page with:
- Active actions
- Saved task templates
- Scheduled automations
- quick actions:
  - New Task
  - New Automation
  - View Active

### 5.2 Abilities
#### Skills
Filters:
- All
- Learned
- Imported
- OpenClaw
- Experimental
- Disabled
- Bundles

#### Connectors
For:
- MCP connectors
- API integrations
- Word/Outlook/SharePoint integrations
- external service connections
- sidecars like LlamaIndex if exposed as provider-level connectors

#### Learn
Learn is the capability-learning workflow area.

## 6. Clearer Learn page and capability-learning UX

### 6.1 Learn page is a process area, not just a history list
Learn should include:
- active teaching sessions
- active observation sessions
- generated skill proposals awaiting review
- pending learned abilities that are not yet promoted to Skills
- items under revision
- recently accepted/promoted learned skills

### 6.2 Legacy top learning actions (superseded by R11.1 §0B.5)
Top learning actions:
1. Demonstrate Workflow
2. Learn from Recent Run
3. Observe My Actions
4. Figure It Out and Learn
5. Improve Existing Skill

### 6.3 Learn page tabs / sections
Recommended tabs:
- In Progress
- Review
- Pending Abilities
- History

Meanings:
- In Progress = current teaching/observation/exploration runs
- Review = skill proposals, candidates, clarification-needed items
- Experimental = private installs and generated skills active in experimental mode
- Recent = recently accepted/promoted/installed learning outputs
- Sessions = past teach/observe/explore sessions

## 7. Skills, bundles, and micro-skill chaining

### 7.1 Skills can be atomic or composite
```ts
export const SkillStructureKindSchema = z.enum([
  "atomic",
  "composite",
]);
```

### 7.2 Bundles
A bundle is a grouping/distribution artifact for skills and support material.
Examples:
- Bitwig Core
- Word Core
- Outlook Workflows
- Litigation Drafting

A bundle can contain:
- atomic skills
- composite skills
- support packs
- references
- shortcut/MIDI maps
- connector hints

Bundles are primarily a grouping/viewing/distribution construct, not the main everyday UI object.

### 7.3 Micro-skill chaining
Skills and tasks should support chaining using a shared workflow graph under the hood.

## 8. Shared workflow graph model for tasks and skills

Tasks and skills remain separate top-level object types, but compile down to the same underlying workflow graph IR.

```ts
export const WorkflowGraphSchema = z.object({
  graph_id: z.string().uuid(),
  nodes: z.array(z.object({
    node_id: z.string().uuid(),
    step_kind: z.string().max(64),
    title: z.string().max(160),
    config: z.record(z.string(), z.unknown()).default({}),
  })),
  edges: z.array(z.object({
    from: z.string().uuid(),
    to: z.string().uuid(),
    condition: z.string().optional(),
  })),
  schema_version: z.literal(1),
});
```

Tasks are still:
- specific action objects
- user-run or scheduled workflows
- more output/deadline/form oriented

Skills are still:
- reusable abilities/templates
- callable from tasks or directly
- candidate for packaging/bundles

## 9. Step taxonomy: internal-rich, user-simple

### 9.1 Keep the rich internal step taxonomy
Required internal step kinds:
- action_step
- guard_step
- branch_step
- decision_step
- reasoning_step
- verification_step
- fallback_step
- subworkflow_step
- loop_step
- ask_user_step

### 9.2 Hide most of the type burden from users
By default, the user should enter or review plain-language steps.
The system/ELNOR should infer the likely step kind.

Examples:
- “If there is no instrument track, create one.” -> guard_step + action_step
- “Write the memo” -> reasoning_step
- “Ask me if there are multiple equally good pianos” -> ask_user_step / decision_step

### 9.3 Advanced edit override
Advanced mode should let the user override step type if needed.

## 10. Execution modes and learning modes

### 10.1 Execution modes
- strict = fixed sequence, minimal deviation
- guided = follow structure, bounded local inference allowed, can use declared fallbacks
- adaptive = goal-constrained, can restructure plan shape within policy, still must satisfy verification and guardrails

Adaptive mode does not remove verification checkpoints.

### 10.2 Learning overlays
Recommended internal learning overlays:
- demonstrate_workflow
- learn_from_recent_run
- observe_my_actions
- explore_and_learn
- improve_existing_skill

These are not execution modes; they are how a skill gets created or improved.

## 11. Observation and teaching controls

### 11.1 Explicit teaching state machine
Need states:
- Armed
- Capturing
- Paused
- Stopped
- Reviewing
- Cancelled

### 11.2 Observation outside ELNOR
Must remain:
- explicit
- scoped
- visible
- user-controlled

### 11.3 Teaching controls in UI
Top controls on Learn page:
- Start Demonstration
- Learn from Recent Run
- Start Observation
- Explore and Learn
- Improve Existing Skill

Each should have a short subtitle/tool tip explaining exactly what it does.

## 12. Categories UI and interaction

### 12.1 Category model
Categories are:
- overlapping
- color-coded
- user-defined
- multi-assignable

### 12.2 Right-click requirement
Right-click on eligible objects must include:
Add to Category…

### 12.3 Browser behavior
Browser should show:
- Starred
- Categories
- filtered categories based on current root if desired
- search within categories

## 13. Skills page details

### Skills page filters
- All
- Learned
- Imported
- OpenClaw
- Experimental
- Disabled
- Bundles

### Skill detail drawer
Show:
- name
- purpose
- app/system
- source
- status/health
- category tags
- bundle membership
- triggers / negative triggers
- preferred control surfaces
- dependencies/connectors
- edit / duplicate / disable / delete
- add to category

## 14. Connectors page details

Connectors page should show:
- provider/service
- auth state
- health
- data class
- policy toggles
- linked skills
- recent receipts
- tests / smoke tests

Why separate from Skills:
Connectors are formal access paths, not reusable behavior templates.

## 15. Learn page details

Top area:
- Demonstrate Workflow
- Learn from Recent Run
- Observe My Actions
- Figure It Out and Learn
- Improve Existing Skill

Below:
- proposals awaiting review
- in-progress sessions
- active experimental skills
- clarification-needed items
- recently accepted/promoted items

Learned items should move into Skills/Connectors when accepted, but remain reviewable in Learn as recent activity/history.

## 16. What should not be a first-class user-facing subcategory

Do not make these first-class browser categories under Abilities:
- Tools
- Plugins
- Native OpenClaw primitives

They should instead appear:
- in skill/connecter details
- in advanced/admin/dev views
- in diagnostics/settings if needed

## 17. Cross-doc incorporation notes

The detailed menu layout, window layout, and task page specifics likely belong primarily in:
- Q Core UI specs
- ELNOR Core / EC Core task workflow specs

But DOC3 should still record:
- skill vs connector distinction
- learn page semantics
- bundle semantics
- category behavior
- shared workflow-graph relationship to tasks
- execution modes
- learning overlays
- step-type internal model
- micro-skill chaining
- review/install lane integration

Other docs likely needing updates:
- Q Core UI spec
- ELNOR Core / EC Core specs
- DOC10
- DOC11
- DOC7
- DOC15
- DOC9

## 18. Suggested acceptance criteria

The next DOC3/Q/Core revisions should make all of the following true:

1. The user can manage everything from a unified Actions & Abilities area.
2. The browser acts as a flexible organizer, not a mandatory gate.
3. Tasks remain a distinct coded object.
4. Skills remain distinct from connectors.
5. Learn is clearly a capability-learning process area, not generic self-learning.
6. Generated/experimental skills can be managed from Learn and then move into Skills.
7. Categories are overlapping, color-coded, and assignable from right-click.
8. Skills can be atomic or composite.
9. Tasks and skills share one internal workflow graph model.
10. The user does not have to manually choose internal step types unless in advanced mode.
11. Guided and adaptive modes are clearly distinct.
12. Learning overlays are clearly named and distinct.
13. The main pane can always show focused views directly for Actions, Tasks, Automations, Skills, Connectors, and Learn.

## 19. Bottom line recommendation

For the next revision:
- keep Actions & Abilities as the overall user-facing area
- keep Actions / Abilities / Learn conceptually unified
- keep Tasks as the specific coded action object
- keep Skills / Connectors / Learn as the Abilities subcategories
- use Categories as the overlapping user-defined grouping system
- keep bundles mostly as technical/grouping artifacts
- keep the rich internal step taxonomy, but hide it from the default user flow
- treat tasks and skills as different top-level object types over one shared workflow graph

This is the clearest model so far and should be folded into the next DOC3/Q/Core revision pass.


---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 3 — Merged Addendum — Self-Learning / Guided Learning Patch Plan R2


# DOC3 Self-Learning / Guided Learning Patch Plan R2

**Purpose:** define the full patch set needed to make DOC3’s self-learning, guided learning, observation mode, proposal lifecycle, and later-use path actually shippable.  
**Primary target:** next DOC3 revision after R9.  
**Companion scope:** ELNOR Core, DOC4, DOC7, DOC8, DOC9, DOC10, DOC11, DOC12, DOC15, Q Core / UI, and the consolidated DOC3 companion delta plan.  
**Status:** normative patch-plan proposal.

---

## 0. Executive verdict

R1 correctly identified the problem and most missing concepts.  
R2 closes the remaining gaps that would otherwise make the shipped product fail in practice.

**Core conclusion:** the learning system will only work if the next DOC3 revision explicitly specifies, end to end:

1. how a learning mode is entered,
2. what exact runtime object represents the learning session,
3. what events are captured and by which adapters,
4. how boundaries are set and noise is trimmed,
5. how meaning is attached to the observed/measured workflow,
6. how a proposal is drafted and tested,
7. how that proposal becomes an installed ability,
8. how Elnor becomes aware of that new ability later,
9. how the user sees receipts and lifecycle state throughout,
10. how partial deployment and degraded dependencies fail safely.

This patch plan is intentionally more detailed than R1 so coding agents do not guess.

---

## 1. Guiding rules

### 1.1 Invariants preserved
- **OpenClaw-native-first**
- **EC single writer**
- **Q is not a second runtime authority**
- **Skills are reusable abilities, not raw runtime primitives**
- **Project/matter identity remains outside DOC3**
- **Observation outside ELNOR is explicit, bounded, and visible**
- **Generated abilities are proposals first, not silent shared installs**

### 1.2 New rules added in R2
1. Learning surfaces and learning mechanisms are separate concepts.
2. Every learning entry point must map to a concrete LearnSession lifecycle.
3. Learned abilities are not usable later unless they appear in a runtime-readable Ability Availability Snapshot.
4. Learning without receipts is considered incomplete.
5. Teaching by example requires both:
   - low-level observed action traces
   - high-level semantic labeling / clarification
6. A generated skill that cannot be activated, deactivated, or quarantined is incomplete.
7. A learned ability must never bypass bridge rebuild / registry update before becoming later-usable.

---

## 2. What R1 was still missing

R1 already covered:
- entry points,
- observation sources,
- install lanes,
- proposal artifacts,
- UI ideas,
- companion-doc impacts.

But R1 still lacked complete treatment of:

- unified Learn runtime lifecycle,
- one canonical LearningQueryContextPayload pattern,
- proposal pinning / review continuity,
- document dependency declarations for learning/review agents,
- runtime Ability Availability Snapshot,
- explicit activation and deactivation commands,
- capability/ability later-use chain,
- correlated LearningReceipts across the whole lifecycle,
- partial deployment behavior for each learning entry point.

R2 fixes those.

---

## 3. New major DOC3 section to add

Add a new major section early in DOC3:

# §0A Learning Runtime, Guided Learning, and Later-Use Activation

This section should precede the app-family sections and should be the normative owner for:

- learning entry points,
- learning session state,
- observation adapters,
- proposal lifecycle,
- install lanes,
- runtime availability of learned abilities,
- learning telemetry,
- user controls,
- and later-use activation.

---

## 4. Unified learning runtime model

### §0A.1 Learning surfaces vs learning mechanisms

**Learning surfaces** are what the user sees/clicks:
- Demonstrate Workflow
- Learn from Recent Run
- Observe My Actions
- Figure It Out and Learn
- Improve Existing Skill

**Learning mechanisms** are what the system actually does:
- create LearnSession
- arm capture
- collect traces / observation events
- normalize and cluster
- ask clarifying questions
- build proposal
- run validation/tests
- choose install lane
- materialize/install
- rebuild bridge
- update Ability Availability Snapshot
- emit receipts

These must not remain blended or ambiguous.

### §0A.2 Canonical LearnSession state machine

```ts
// packages/contracts/src/learning/learn-session.ts
import { z } from "zod";

export const LearningEntryPointSchema = z.enum([
  "demonstrate_workflow",
  "learn_from_recent_run",
  "observe_my_actions",
  "figure_it_out_and_learn",
  "improve_existing_skill",
]);

export const LearnExecutionModeSchema = z.enum([
  "strict",
  "guided",
  "adaptive",
]);

export const LearnSessionStateSchema = z.enum([
  "idle",
  "armed",
  "capturing",
  "paused",
  "stopped",
  "reviewing",
  "awaiting_questions",
  "drafting_proposal",
  "testing_proposal",
  "ready_for_review",
  "installing_private",
  "installed_private",
  "approved",
  "rejected",
  "cancelled",
  "quarantined",
]);

export const LearnSessionSchema = z.object({
  learn_session_id: z.string().uuid(),
  entry_point: LearningEntryPointSchema,
  execution_mode: LearnExecutionModeSchema.default("guided"),
  state: LearnSessionStateSchema,
  actor_mode: z.enum(["user_driven", "elnor_driven", "mixed"]).default("mixed"),
  project_id: z.string().max(120).optional(),
  room_id: z.string().max(160).optional(),
  active_trace_ids: z.array(z.string().uuid()).default([]),
  active_observation_batch_ids: z.array(z.string().uuid()).default([]),
  proposal_id: z.string().uuid().optional(),
  start_boundary_ref: z.string().max(240).optional(),
  stop_boundary_ref: z.string().max(240).optional(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

**Required transitions**
```text
idle -> armed
armed -> capturing | cancelled
capturing -> paused | stopped | cancelled
paused -> capturing | stopped | cancelled
stopped -> reviewing | cancelled
reviewing -> awaiting_questions | drafting_proposal | cancelled
awaiting_questions -> drafting_proposal | cancelled
drafting_proposal -> testing_proposal | ready_for_review | cancelled
testing_proposal -> ready_for_review | quarantined | cancelled
ready_for_review -> installing_private | approved | rejected | cancelled
installing_private -> installed_private | quarantined | cancelled
installed_private -> approved | quarantined | cancelled
approved -> idle
rejected -> idle
cancelled -> idle
quarantined -> reviewing | rejected
```

### §0A.3 LearnSession creation contract

```ts
// packages/contracts/src/learning/create-learn-session.ts
import { z } from "zod";
import { LearningEntryPointSchema, LearnExecutionModeSchema, LearnSessionSchema } from "./learn-session";

export const CreateLearnSessionRequestSchema = z.object({
  entry_point: LearningEntryPointSchema,
  execution_mode: LearnExecutionModeSchema.default("guided"),
  project_id: z.string().max(120).optional(),
  room_id: z.string().max(160).optional(),
  target_skill_id: z.string().max(120).optional(),
  target_app_family: z.string().max(80).optional(),
  ask_before_build: z.boolean().default(false),
  allow_private_install: z.boolean().default(false),
  observation_scope: z.enum(["none", "supported_apps_only", "all_enabled_observation_sources"]).default("none"),
});

export const CreateLearnSessionResponseSchema = z.object({
  session: LearnSessionSchema,
  ui_mode: z.enum(["overlay", "drawer", "recent-run", "exploratory"]),
  next_action: z.enum(["wait_for_first_meaningful_event", "show_recent_run_picker", "launch_exploration", "show_skill_delta_editor"]),
});
```

---

## 5. Exact semantics for each learning surface

### §0A.4 Legacy Demonstrate Workflow (canonical user-facing replacement: Demonstrating Skill)
**What it means**
- explicit bounded teaching session
- the user intentionally demonstrates a workflow
- session begins in `armed`
- first meaningful event begins `capturing`
- the user explicitly stops capture
- system enters `reviewing`

**Required UI**
- Start Teach
- Pause
- Stop
- Cancel
- Mark Step
- Mark Goal

**Required output**
- one LearnSession
- zero or more traces / observed action events
- a review timeline
- a draftable candidate

### §0A.5 Learn from Recent Run
**What it means**
- user chooses from recent eligible runs/traces rather than performing a new live session
- no observation mode required
- review starts from existing traces

**Required picker behavior**
- show only eligible runs:
  - successful or mostly successful
  - trace-rich
  - not already promoted
  - not already attached to another active proposal
- allow selecting one run or a set of compatible runs

**Required output**
- LearnSession in `reviewing`
- source traces attached
- ability to trim / exclude trace segments before drafting

### §0A.6 Legacy Observe My Actions (canonical treatment: observation option within relevant learning modes)
**What it means**
- explicit user opt-in to observation outside the normal Q interaction flow
- only enabled if supported adapters are healthy
- persistent visible indicator required
- no hidden passive observation by default

**Required behavior**
- create LearnSession in `armed`
- overlay or hotkey controls persist across apps
- first meaningful event starts capture
- all captured data must record source adapter
- session must support pause/stop/cancel
- post-session review is mandatory before proposal generation

### §0A.7 Legacy Figure It Out and Learn (canonical user-facing replacement: Autonomous Agent Practice)
**What it means**
- exploratory learning mode
- user supplies a goal
- Elnor attempts to solve it using allowed capabilities/tools
- if verified success is reached, it creates a proposal

**Required controls**
- exploration budget
- allowed tool surfaces
- max retries
- success verification model
- user confirmation step when confidence is below threshold

**Required guardrail**
- no shared install directly from exploratory mode
- private experimental install only if policy and validation permit it

### §0A.8 Improve Existing Skill
**What it means**
- create a delta proposal against an existing skill
- never edit the canonical skill in place without proposal/review

**Required behavior**
- select existing skill
- capture new traces or use recent runs
- generate diff against current skill
- test the proposed delta
- review and either merge or reject

---

## 6. Observation adapter architecture

### §0A.9 Supported adapter matrix
The next DOC3 revision must define adapter contracts explicitly.

#### Required adapter kinds
```ts
// packages/contracts/src/learning/observation-adapter.ts
import { z } from "zod";

export const ObservationAdapterKindSchema = z.enum([
  "browser_dom",
  "browser_snapshot",
  "accessibility_ui",
  "keyboard_shortcut",
  "text_entry",
  "mouse_click",
  "clipboard",
  "file_dialog",
  "process_launch",
  "terminal_command",
  "midi",
  "mcp_receipt",
  "wrapper_receipt",
]);

export const ObservationAdapterStatusSchema = z.object({
  adapter_id: z.string().max(80),
  kind: ObservationAdapterKindSchema,
  enabled: z.boolean(),
  healthy: z.boolean(),
  last_seen_at: z.string().datetime().optional(),
  last_error_code: z.string().max(120).optional(),
  schema_version: z.literal(1),
});
```

#### Required event schema
```ts
// packages/contracts/src/learning/observed-action.ts
import { z } from "zod";
import { ObservationAdapterKindSchema } from "./observation-adapter";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const ObservedActionEventSchema = z.object({
  event_id: z.string().uuid(),
  learn_session_id: z.string().uuid(),
  adapter_kind: ObservationAdapterKindSchema,
  app_family: z.string().max(80).optional(),
  window_title: z.string().max(200).optional(),
  action_label: z.string().max(240),
  control_surface: ControlSurfaceSchema.optional(),
  target_ref: z.string().max(240).optional(),
  before_state_ref: z.string().max(240).optional(),
  after_state_ref: z.string().max(240).optional(),
  caused_state_change: z.boolean().default(false),
  redaction_mode: z.enum(["none", "mask_text", "hash_only"]).default("mask_text"),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0A.10 Adapter rules
- **browser_dom** / **browser_snapshot**: for web tasks and page knowledge
- **accessibility_ui**: for desktop app UI meaning where available
- **keyboard_shortcut**: capture shortcut key combos as named events
- **text_entry**: never capture raw text by default; use masked or hashed representation unless elevated session mode is allowed
- **mouse_click**: must attach window/app and target metadata when possible; coordinates alone are insufficient
- **clipboard**: capture copy/paste event class, not raw payload by default
- **file_dialog**: record selected file/folder path (redacted where needed)
- **process_launch** / **terminal_command**: record executable/command and result class
- **midi**: record MIDI message class and mapped target if known
- **mcp_receipt** / **wrapper_receipt**: convert existing connector/wrapper traces into observation events

### §0A.11 Observation privacy boundary
Observation mode must:
- be opt-in
- show persistent UI indicator
- support stop/cancel from outside Q
- keep raw observations as proposal inputs, not canonical memory
- redact or hash text entry by default
- retain only the minimal event trail needed for proposal generation unless explicitly escalated

---

## 7. Boundary, noise trimming, and semantic labeling

### §0A.12 Boundary semantics
**Armed** means capture has not yet started.  
Capture starts on the first **meaningful event**.

**Meaningful events include**
- shortcut invocation
- menu command
- file/folder selection
- connector call
- process launch
- UI action causing state change
- explicit mark-step/mark-goal

**Not meaningful by default**
- mouse move
- hover
- focus change
- scroll without state change
- returning to Q after stop

### §0A.13 Boundary objects
```ts
// packages/contracts/src/learning/boundary.ts
import { z } from "zod";

export const WorkflowBoundarySchema = z.object({
  boundary_id: z.string().uuid(),
  learn_session_id: z.string().uuid(),
  kind: z.enum(["start", "pause", "resume", "stop", "cancel", "trim_start", "trim_end"]),
  source: z.enum(["user_click", "hotkey", "system_inferred"]),
  source_event_id: z.string().uuid().optional(),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0A.14 Semantic labeling and clarification
After capture stops, the system must gather or infer:
- proposed skill name
- goal summary
- use conditions
- non-use conditions
- project-specific vs reusable
- preferred control surface order
- package/collection placement
- success conditions

If confidence is low, the system must enter `awaiting_questions`.

```ts
// packages/contracts/src/learning/clarification.ts
import { z } from "zod";

export const LearningQuestionSchema = z.object({
  question_id: z.string().uuid(),
  proposal_id: z.string().uuid(),
  field: z.enum([
    "name",
    "goal_summary",
    "use_conditions",
    "non_use_conditions",
    "project_scope",
    "preferred_surface",
    "package_assignment",
    "success_condition",
  ]),
  prompt: z.string().max(400),
  required: z.boolean().default(true),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});

export const LearningAnswerSchema = z.object({
  question_id: z.string().uuid(),
  proposal_id: z.string().uuid(),
  answer: z.string().max(2000),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

---

## 8. Shared workflow graph and object model

### §0A.15 Shared workflow IR
Tasks, skills, proposals, and automations should share one workflow graph representation.

```ts
// packages/contracts/src/workflows/workflow-graph.ts
import { z } from "zod";
import { ControlSurfaceSchema } from "../capabilities/control-surface";

export const WorkflowStepTypeSchema = z.enum([
  "action",
  "guard",
  "branch",
  "decision",
  "reasoning",
  "verification",
  "fallback",
  "subworkflow",
  "loop",
  "ask_user",
]);

export const WorkflowStepSchema = z.object({
  step_id: z.string().max(120),
  step_type: WorkflowStepTypeSchema,
  label: z.string().max(240),
  description: z.string().max(800).optional(),
  preferred_surfaces: z.array(ControlSurfaceSchema).default([]),
  next_step_ids: z.array(z.string().max(120)).default([]),
  verification_rule_ref: z.string().max(240).optional(),
  schema_version: z.literal(1),
});

export const WorkflowGraphSchema = z.object({
  workflow_id: z.string().max(120),
  title: z.string().max(200),
  entry_step_id: z.string().max(120),
  steps: z.array(WorkflowStepSchema).min(1),
  schema_version: z.literal(1),
});
```

### §0A.16 Object types over the shared graph
Keep distinct top-level types:
- `SkillTemplate`
- `TaskTemplate`
- `TaskInstance`
- `AutomationDefinition`
- `SkillBundleProposal`

The graph is shared; the object semantics are not.

---

## 9. Proposal lifecycle, validation, and install lanes

### §0A.17 Skill bundle proposal
```ts
// packages/contracts/src/skills/proposal.ts
import { z } from "zod";

export const SkillBundleProposalSchema = z.object({
  proposal_id: z.string().uuid(),
  learn_session_id: z.string().uuid().optional(),
  source_trace_ids: z.array(z.string().uuid()).min(1),
  source_mode: z.enum(["elnor_observed", "user_guided", "desktop_observed", "exploratory"]),
  draft_skill_path: z.string().max(240),
  draft_manifest_path: z.string().max(240),
  draft_graph_ref: z.string().max(240),
  proposal_state: z.enum([
    "draft",
    "awaiting_questions",
    "testing",
    "ready_for_review",
    "installing_private",
    "installed_private",
    "approved",
    "rejected",
    "cancelled",
    "quarantined",
  ]),
  install_lane: z.enum(["pending", "experimental_private", "approved_workspace", "shared_promoted"]).default("pending"),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0A.18 Validation report
```ts
// packages/contracts/src/skills/validation.ts
import { z } from "zod";

export const SkillValidationReportSchema = z.object({
  proposal_id: z.string().uuid(),
  lint_passed: z.boolean(),
  trigger_tests_passed: z.boolean(),
  dry_run_passed: z.boolean(),
  canary_passed: z.boolean().default(false),
  dependency_check_passed: z.boolean(),
  collision_check_passed: z.boolean(),
  safety_class: z.enum(["low", "medium", "high"]),
  issues: z.array(z.string().max(240)).default([]),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0A.19 Activation / deactivation / quarantine
A learned ability is incomplete unless it can later be activated, deactivated, or quarantined.

```ts
// packages/contracts/src/capabilities/activation.ts
import { z } from "zod";

export const AbilityLifecycleActionSchema = z.enum([
  "activate",
  "deactivate",
  "promote_shared",
  "demote_private",
  "quarantine",
  "unquarantine",
  "archive",
]);

export const AbilityLifecycleCommandSchema = z.object({
  ability_id: z.string().max(160),
  action: AbilityLifecycleActionSchema,
  reason: z.string().max(240).optional(),
  requested_by: z.enum(["user", "system", "repair"]),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

---

## 10. Later-use chain and runtime ability awareness

### §0A.20 Ability Availability Snapshot
This is one of the most important missing pieces and must be added.

```ts
// packages/contracts/src/capabilities/availability-snapshot.ts
import { z } from "zod";

export const AbilityAvailabilitySnapshotSchema = z.object({
  ability_id: z.string().max(160),
  title: z.string().max(200),
  family: z.string().max(80),
  install_lane: z.enum(["pending", "experimental_private", "approved_workspace", "shared_promoted", "quarantined"]),
  enabled: z.boolean(),
  usable_now: z.boolean(),
  reason_unusable: z.string().max(240).optional(),
  preferred_route_kinds: z.array(z.string().max(120)).default([]),
  project_scopes: z.array(z.string().max(120)).default([]),
  trigger_phrases: z.array(z.string().max(200)).default([]),
  negative_triggers: z.array(z.string().max(200)).default([]),
  schema_version: z.literal(1),
});
```

### §0A.21 Required later-use chain
The next DOC3 revision must state the later-use chain explicitly:

```text
LearnSession
-> SkillBundleProposal
-> SkillValidationReport
-> install lane selected
-> skill bundle materialized
-> capability manifest materialized
-> Ability Availability Snapshot rebuilt
-> capability bridge rebuilt
-> DOC10 awareness updated
-> Q Abilities view updated
-> Elnor can route to the ability later
```

### §0A.22 Required runtime lookup rule
At runtime, Elnor must:
1. classify intent
2. resolve project/context/app
3. query Ability Availability Snapshot
4. determine whether a matching ability exists
5. choose between:
   - approved/shared skill
   - experimental/private skill
   - task template
   - connector-backed capability
   - direct native action
   - exploratory learning mode

Without this explicit lookup, learned skills may never be used.

---

## 11. Learning query context payload, proposal pinning, and document dependencies

### §0A.23 LearningQueryContextPayload
Borrowing the deep-link payload pattern previously used elsewhere, the Learn system needs one too.

```ts
// packages/contracts/src/learning/query-context.ts
import { z } from "zod";

export const LearningQueryContextPayloadSchema = z.object({
  source_surface: z.string().max(120),
  source_id: z.string().max(160).optional(),
  learn_session_id: z.string().uuid().optional(),
  proposal_id: z.string().uuid().optional(),
  trace_ids: z.array(z.string().uuid()).default([]),
  cluster_id: z.string().uuid().optional(),
  project_id: z.string().max(120).optional(),
  app_family: z.string().max(80).optional(),
  current_state: z.string().max(80).optional(),
  referenced_ids: z.array(z.string().max(160)).default([]),
  schema_version: z.literal(1),
});
```

This payload is what lets any “Why?”, “Revise”, “Use this”, “Explain”, or “Continue review” action reopen the exact learning object.

### §0A.24 Proposal pinning
Active learning targets must not be lost in long review sessions.

Add a pinning rule:
- the active learning proposal is pinned while being reviewed
- current questions/answers are pinned
- current test report is pinned
- previous review rounds may compress after review but remain accessible

### §0A.25 Document dependency declarations
The learning builder / reviewer must be able to declare which support packs / docs / capability refs are required.

```ts
// packages/contracts/src/learning/document-deps.ts
import { z } from "zod";

export const LearningDocumentDependencySchema = z.object({
  proposal_id: z.string().uuid(),
  dependency_kind: z.enum(["support_pack", "app_manual", "existing_skill", "capability_manifest", "test_artifact"]),
  dependency_ref: z.string().max(240),
  required: z.boolean().default(true),
  schema_version: z.literal(1),
});
```

---

## 12. Receipts, telemetry, and user visibility

### §0A.26 Learning receipts
```ts
// packages/contracts/src/learning/receipts.ts
import { z } from "zod";

export const LearningReceiptSchema = z.object({
  receipt_id: z.string().uuid(),
  learn_session_id: z.string().uuid().optional(),
  proposal_id: z.string().uuid().optional(),
  event_kind: z.enum([
    "session_started",
    "capture_started",
    "capture_paused",
    "capture_stopped",
    "proposal_drafted",
    "questions_requested",
    "tests_started",
    "tests_passed",
    "tests_failed",
    "installed_private",
    "approved",
    "rejected",
    "cancelled",
    "quarantined",
    "used_later",
  ]),
  correlation_id: z.string().max(200),
  created_at: z.string().datetime(),
  schema_version: z.literal(1),
});
```

### §0A.27 Required UI telemetry
The user must be able to see:
- learning started
- learning active
- learning paused
- learning stopped
- proposal created
- questions requested
- testing running
- tests passed/failed
- installed privately
- promoted shared
- later-use of the learned ability
- degraded/quarantined status

---

## 13. Required UI/UX surfaces

### §0A.28 Learn page / panel requirements
The next DOC3 revision must require Q to implement:

1. **Learn home / dashboard**
2. **Teach session overlay or global control**
3. **Recent run picker**
4. **Observation mode control panel**
5. **Exploratory learning launch panel**
6. **Proposal review drawer**
7. **Question / answer panel**
8. **Timeline trim/review UI**
9. **Experimental/private learned abilities list**
10. **Learning receipts/activity feed**

### §0A.29 Required UI states
For each learning surface:
- loading
- empty
- armed
- capturing
- paused
- stopped
- reviewing
- awaiting_questions
- testing
- ready_for_review
- installing_private
- installed_private
- approved
- rejected
- cancelled
- quarantined
- partial_deployment

### §0A.30 Required actions
- start
- pause
- resume
- stop
- cancel
- mark step
- mark goal
- trim start/end
- generate proposal
- answer questions
- test
- install privately
- approve
- reject
- revise
- quarantine
- unquarantine

---

## 14. Required API surface

### §0A.31 Learning routes
```http
POST /api/learn/session/start
POST /api/learn/session/pause
POST /api/learn/session/resume
POST /api/learn/session/stop
POST /api/learn/session/cancel
GET  /api/learn/session/:id

POST /api/learn/session/:id/mark-goal
POST /api/learn/session/:id/mark-step
POST /api/learn/session/:id/trim

POST /api/learn/session/:id/generate-proposal
GET  /api/learn/proposals
GET  /api/learn/proposals/:id
POST /api/learn/proposals/:id/questions
POST /api/learn/proposals/:id/answers
POST /api/learn/proposals/:id/test
POST /api/learn/proposals/:id/install-private
POST /api/learn/proposals/:id/approve
POST /api/learn/proposals/:id/reject
POST /api/learn/proposals/:id/revise

GET  /api/learn/receipts
GET  /api/abilities/availability
POST /api/abilities/:id/activate
POST /api/abilities/:id/deactivate
POST /api/abilities/:id/quarantine
POST /api/abilities/:id/promote-shared
```

### §0A.32 Required middleware
- authenticated session required for all read routes
- elevated permissions for install/promote/quarantine
- observation routes require explicit observation entitlement
- if auth or policy missing, fail closed

---

## 15. Required modules and file paths

### Backend
```text
apps/ec-service/src/learning/session-service.ts
apps/ec-service/src/learning/state-machine.ts
apps/ec-service/src/learning/observation-adapters/browser.ts
apps/ec-service/src/learning/observation-adapters/accessibility.ts
apps/ec-service/src/learning/observation-adapters/shortcut.ts
apps/ec-service/src/learning/observation-adapters/midi.ts
apps/ec-service/src/learning/observation-adapters/process.ts
apps/ec-service/src/learning/trace-normalizer.ts
apps/ec-service/src/learning/cluster-service.ts
apps/ec-service/src/learning/clarification-service.ts
apps/ec-service/src/learning/proposal-builder.ts
apps/ec-service/src/learning/test-runner.ts
apps/ec-service/src/learning/install-service.ts
apps/ec-service/src/learning/receipt-writer.ts

apps/ec-service/src/capabilities/ability-availability-snapshot.ts
apps/ec-service/src/capabilities/ability-lookup.ts
apps/ec-service/src/capabilities/install-lane-filter.ts
apps/ec-service/src/capabilities/activation-service.ts
```

### Frontend
```text
apps/q-frontend/src/features/learn/LearnHomePage.tsx
apps/q-frontend/src/features/learn/LearnSessionOverlay.tsx
apps/q-frontend/src/features/learn/LearnSessionTimeline.tsx
apps/q-frontend/src/features/learn/RecentRunPicker.tsx
apps/q-frontend/src/features/learn/ObservationModeBanner.tsx
apps/q-frontend/src/features/learn/ProposalReviewDrawer.tsx
apps/q-frontend/src/features/learn/LearningQuestionsPanel.tsx
apps/q-frontend/src/features/learn/PendingAbilitiesView.tsx
apps/q-frontend/src/features/learn/LearningReceiptsFeed.tsx
apps/q-frontend/src/features/abilities/AbilityAvailabilityPanel.tsx
apps/q-frontend/src/features/abilities/AbilityActivationControls.tsx
```

### Storage
```text
ELNOR_MEMORY/system/learning/sessions.jsonl
ELNOR_MEMORY/system/learning/proposals.jsonl
ELNOR_MEMORY/system/learning/questions.jsonl
ELNOR_MEMORY/system/learning/answers.jsonl
ELNOR_MEMORY/system/learning/validation_reports.jsonl
ELNOR_MEMORY/system/learning/receipts.jsonl
ELNOR_MEMORY/system/abilities/availability_current.json
ELNOR_MEMORY/system/abilities/activation_history.jsonl
```

---

## 16. Acceptance tests

### §0A.33 Minimum acceptance suite additions
1. Demonstrate Workflow end-to-end
2. Learn from Recent Run end-to-end
3. Observe My Actions end-to-end
4. Figure It Out and Learn end-to-end
5. Improve Existing Skill delta flow
6. proposal -> test -> install-private -> later-use
7. proposal -> approve -> shared -> later-use
8. quarantine + later-use denied
9. ability availability snapshot refresh after install
10. receipts present at every lifecycle stage
11. learning query context payload reopens exact proposal/session
12. pinned active proposal survives long review session
13. partial deployment surfaces explicit degraded state

---

## 17. Partial deployment rules

If the next DOC3 revision ships before all companion changes:
- learning surfaces may exist but must show `partial_deployment`
- install actions must be disabled if ability availability snapshot is unavailable
- observation mode must be disabled if adapter truth is unavailable
- proposal review may still exist, but promotion/install must fail closed
- “later use” of learned abilities must be clearly marked unavailable

---

## 18. Final rule

The next DOC3 revision is not complete until the learning system can truthfully answer all of these questions at runtime:

1. What learning mode is being used?
2. What session is active?
3. What data is being observed?
4. What is the workflow boundary?
5. What meaning was attached to the workflow?
6. What proposal was generated?
7. What tests were run and with what outcome?
8. What install lane is it in?
9. Is the learned ability actually available now?
10. Can Elnor find and use it later?
11. Can the user see all of this with receipts and telemetry?


---

> LEGACY / NON-OPERATIVE: the preserved merged body below is retained for lineage and traceability. Implement from the authoritative R11.2 / R9.2 Part 0 sections above, not from the preserved historical text below.

# Part 4 — Merged Appendix — ELNOR First-Wave MCP Pack R3

# ELNOR First-Wave MCP Pack R3

**Purpose:** fast-moving connector appendix for `DOC3 App Skills R8`.  
**Status:** companion rollout and design pack.  
**Rule:** vendor details here may evolve faster than DOC3 core. Treat availability, preview, and auth notes as operational guidance, not static architecture truth.

## 1. Design rules for the pack

1. Prefer MCP when the source of truth is a cloud or enterprise system and a healthy connector exists.
2. Prefer local skill surfaces for local desktop apps and UI-heavy workflows.
3. Do not let connector availability silently override user/provider policy.
4. Every connector route must emit receipts and health state.
5. Imported or generated skills may reference these connectors only through capability manifests / bindings, not by embedding secrets.

## 2. Wave 1A — Microsoft family

### 2.1 Microsoft 365 Search
**Use for**
- broad discovery across Microsoft 365 content
- “find the latest memo”, “search all documents related to X”, “search emails + files”

**Typical capabilities**
- `m365.search`
- `m365.search_people_context`
- `m365.search_recent_project_artifacts`

### 2.2 SharePoint / OneDrive
**Use for**
- project / matter document lookup
- file metadata
- authoritative document retrieval
- cloud source-of-truth bindings

**Typical capabilities**
- `m365.sharepoint_list_documents`
- `m365.onedrive_fetch_file`
- `m365.project_get_latest_document`

### 2.3 Outlook Mail
**Use for**
- search mail
- retrieve thread/message metadata
- draft or send mail only when policy allows

**Typical capabilities**
- `m365.outlook_mail_search`
- `m365.outlook_get_thread`
- `m365.outlook_draft_reply`

### 2.4 Outlook Calendar
**Use for**
- check availability
- create/update meetings
- inspect event details

**Typical capabilities**
- `m365.calendar_check_availability`
- `m365.calendar_create_event`
- `m365.calendar_update_event`

### 2.5 Word
**Use for**
- cloud document content/comments retrieval
- source-of-truth inspection for SharePoint/OneDrive-backed documents

**Typical capabilities**
- `m365.word_fetch_content`
- `m365.word_fetch_comments`

**Note**
- local Word Compare/edit flows still belong to the local DOC3 Word family.

### 2.6 Teams
**Use for**
- post status updates
- read chats/channels (if allowed)
- manage meeting-related messaging

**Typical capabilities**
- `m365.teams_post_message`
- `m365.teams_get_channel_context`

### 2.7 Excel and OneNote watchlist
**Current policy**
- keep as tracked Phase 2 items unless companion vendor validation confirms strong connector coverage.
- if good connector/API coverage appears, add them under the same hybrid rules as the rest of Microsoft.

### Microsoft family default policy
- Reads/searches: default `enabled` or `ask_first` depending on data class
- Writes/sends/schedules: default `ask_first`
- Provider-specific controls required
- Project-bound SharePoint/OneDrive retrieval should be first operational priority

## 3. Wave 1B — Runtime/client targets

### 3.1 OpenAI Responses / ChatGPT Apps
**Role**
- MCP-capable runtime / client
- not itself a source-of-truth doc system

**Use**
- allow selected MCP servers when provider policy permits
- ideal for connector-heavy cloud workflows

### 3.2 Claude API / Claude Code
**Role**
- MCP-capable runtime / client
- good fit for portable skill bundles and connector-heavy tasks

### 3.3 Codex
**Role**
- MCP-capable runtime/client for selected tool surfaces
- particularly useful for dev / implementation workflows if allowed

### 3.4 Perplexity / other AI runtimes
**Role**
- later-stage runtime target when official support / policy comfort exists
- treat as provider-controlled client, not as canonical data source

## 4. Wave 1C — Custom ELNOR servers/adapters

### 4.1 ELNOR Knowledge MCP
Expose:
- knowledge search
- document fetch
- doc index / QMD-backed retrieval
- support-pack retrieval

Use for:
- portable internal knowledge access across runtimes

### 4.2 ELNOR Project Resolver / Project Documents MCP
Expose:
- project search
- project metadata
- project document lookup
- project deadlines / contacts (if available)

Use for:
- portable structured access to your project/matter layer without creating duplicate semantics

### 4.3 ELNOR Local Files MCP
Expose only approved local roots:
- synced work folders
- selected project roots
- optional read-only desktop folders

Use for:
- controlled local-data access where provider/runtime requires MCP-style transport

### 4.4 Acrobat custom adapter
Expose:
- text extraction
- OCR
- table extraction
- bundle assembly / review prep
- redaction prep if supported

Use for:
- PDF-heavy workflows that are awkward via browser/UI

### 4.5 ELNOR Admin / Runtime Health MCP
Expose:
- connector health
- capability health
- room/runtime diagnostics
- smoke-test status

Use for:
- operational introspection by trusted runtimes

## 5. Wave 1D — Gmail / Google family

### 5.1 Gmail
**Use for**
- search and read Gmail when native paths are insufficient or absent

### 5.2 Google Drive / Docs
**Use for**
- future file/document retrieval when applicable

**Policy**
- if native/OpenClaw or existing Gmail integrations already satisfy the use case, do not duplicate them gratuitously

## 6. Routing heuristics ELNOR should learn

1. If the request is about a project/matter-backed cloud source of truth and a healthy allowed connector exists, prefer the connector.
2. If the request is about local desktop behavior, prefer local semantic controls.
3. If the connector is denied or degraded, degrade honestly and fall back only to approved alternatives.
4. Browser automation is the fallback, not the default, for cloud systems with good structured connectors.
5. Imported skills may reference connector hints, but the runtime route is still chosen by ELNOR policy and health.

## 7. Suggested default modes

- **Microsoft reads/searches:** enabled or ask-first
- **Microsoft writes/sends:** ask-first
- **External-provider MCP access to confidential connectors:** ask-first or disabled until explicitly approved
- **ELNOR local-files server:** disabled by default; enable per approved root
- **Admin/health server:** limited to trusted runtimes/agents

## 8. Telemetry requirements

Every connector use should log:
- runtime/provider
- server ID
- tool/action
- project ID if relevant
- data class
- approval state
- result status

Q should show:
- connector badges
- receipt chips
- connector health
- provider toggles
- ask-first prompts

## 9. Implementation sequence

### Phase 1
- Microsoft 365 Search
- SharePoint / OneDrive
- Outlook Mail
- Outlook Calendar
- project resolver integration
- connector settings UI
- receipts / health

### Phase 2
- Teams
- Word cloud content
- ELNOR Knowledge MCP
- ELNOR Project Resolver / Project Documents MCP

### Phase 3
- Acrobat adapter
- ELNOR Local Files MCP
- Gmail / Google adapters
- additional runtime targets and finer-grained provider policies