diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,40 @@
 
 ## [Unreleased]
 
+## [0.11.0.0] - 2026-10-28
+
+### Added
+
+- `okf agent assist [PROMPT]` launches a contextual Claude Code or Codex session;
+  `okf assist` remains an alias. `--debug` and `okf agent context [--json]`
+  inspect local bundles, profile guidance, assets, and commands without launch.
+  JSON uses `schemaVersion: 1`; inspection works without a provider or terminal.
+- `okf kit install [NAME]` selects through fzf when the name is omitted.
+  Explicit names bypass terminal probes; cancellation succeeds without installing.
+  Project asset discovery shares ancestor lookup with agent sessions.
+- Embedded help accepts `--width N`, adapts to terminal width up to 140 columns,
+  and preserves default piped bytes. Topic completion, grouped agent/Kit options,
+  and command examples are included.
+- Profile validation reports same-concept presence, timestamp, membership,
+  uniqueness, and nested-vocabulary deviations with indexed paths. Inspection
+  exposes declared constraints and policies; generated documentation shows
+  effective rules.
+
+### Changed
+
+- Unknown help topics print an error and valid alternatives on stderr and exit 1.
+- Interactive launches require a controlling terminal and fail with inspection
+  alternatives when detached; process and terminal resources close on exceptions.
+- Requires GHC >=9.12 (base >=4.21),
+  `mori://shinzui/baikai/packages/baikai` 0.10.0.0,
+  `mori://shinzui/baikai/packages/baikai-kit` 0.4.0.3, and
+  `mori://shinzui/baikai/packages/baikai-claude` and
+  `mori://shinzui/baikai/packages/baikai-openai` 0.7.3.0.
+- **Breaking (library):** `AssistOptions` has an optional prompt and inspection
+  mode; `Command` adds `AgentContext`; `HelpCommand` carries a width option.
+- Requires `okf-core ^>=0.11.0.0`, including the expanded v0.25.0 built-in
+  catalogue from `mori://shinzui/okf-profiles`.
+
 ## [0.10.0.0] - 2026-10-06
 
 ### Added
diff --git a/help/agents.md b/help/agents.md
--- a/help/agents.md
+++ b/help/agents.md
@@ -17,17 +17,35 @@
   Use --shared to make an item available in every agent session, or --tool-only
   to restrict it to sessions OKF launches. Codex custom agents require shared
   visibility or --accept-shared-codex because Codex cannot isolate them.
-  Install requires NAME; OKF does not configure an interactive kit chooser.
+  Omit NAME to choose a skill or subagent with fzf. Explicit NAME bypasses
+  terminal and fzf probes. The picker requires fzf and a controlling terminal;
+  when unavailable it exits 2 before refreshing the repository. Empty kits exit 1.
+  Esc/Ctrl-C cancels with exit 0 and no installation (the Kit engine contract;
+  bundle/concept pickers instead exit 130). Picker errors exit 1.
 
 ASSIST
 
-  okf assist "PROMPT"               Launch an interactive agent session with your
-                                    installed okf skills on its path, starting
-                                    from PROMPT.
-  okf assist --print-command "PROMPT"
-                                    Print the agent command line without
-                                    launching it.
+  okf agent assist                  Open a contextual interactive session.
+  okf agent assist "PROMPT"         Start with an optional task or question.
+  okf agent assist --debug          Print live OKF context without launching.
+  okf agent assist --print-command  Print the launch command without launching.
+  okf agent context [--json]        Inspect the same read-only context snapshot.
 
+  'okf assist' remains an alias with the same flags and agent.assist settings.
+  --debug and --print-command are mutually exclusive. Inspection works without
+  a terminal or installed provider. Launching requires a controlling terminal.
+
+  Context contains absolute bundle/profile paths, available profile guidance,
+  installed OKF kit references and common commands, including which can write.
+  Discovery uses depth four, skips hidden/build/symlink directories, and makes
+  no network requests. Config values and credentials are omitted. Empty and
+  inaccessible optional sources are reported without preventing inspection.
+  JSON is one object with schemaVersion 1, workingDirectory, projectRoot,
+  bundles, profiles, commands, installedAssets and warnings; it has no clock.
+
+  Project assets use the nearest .git, okf-config.dhall or .okf ancestor, falling
+  back to the current directory. Installation and sessions share this policy.
+
   Both Claude Code and Codex can be launched. The provider you choose is the CLI
   okf runs, so that CLI must be installed: 'claude' for Provider.Claude and
   'codex' for Provider.Codex. A missing binary exits 127 and names the one it
@@ -66,7 +84,7 @@
     agent.model                     launches an agent.
     agent.effort
     agent.systemPrompt
-    agent.assist.provider           Per-command settings for 'okf assist'.
+    agent.assist.provider           Settings for both assist spellings.
     agent.assist.model              These win over the shared defaults above.
     agent.assist.effort
     agent.assist.systemPrompt
diff --git a/help/config.md b/help/config.md
--- a/help/config.md
+++ b/help/config.md
@@ -2,7 +2,7 @@
 
 okf config controls command aliases, profile registries, and optional agent
 assistance: where okf kit fetches skills and subagents from, which providers to
-install for, and how okf assist launches an interactive agent session.
+install for, and how okf agent assist launches an interactive agent session.
 
 COMMANDS
 
@@ -70,7 +70,7 @@
                                     renders no flag at all.
   agent.systemPrompt                Extra system prompt, appended to the agent's
                                     own rather than replacing it.
-  agent.assist.*                    The same four fields for 'okf assist' alone.
+  agent.assist.*                    The same four fields for both assist spellings.
                                     These win over the shared defaults above.
   profiles.registries               Ordered registry list okf profile reads by
                                     default. An explicit [] selects none.
@@ -126,3 +126,6 @@
   command parser. 'okf alias list' and 'okf config show' report errors strictly.
   Built-in commands and dash-prefixed startup arguments bypass alias loading.
   See 'okf help aliases' for duplicate handling and expansion boundaries.
+
+  Canonical agent assist and legacy assist share agent.assist.* keys and the
+  same resolution order. agent context never evaluates agent configuration.
diff --git a/help/interactive.md b/help/interactive.md
--- a/help/interactive.md
+++ b/help/interactive.md
@@ -21,7 +21,8 @@
 
   Interactive selection needs the fzf fuzzy finder on PATH and a terminal.
   Without them, an omitted BUNDLE exits 2 and tells you to pass it explicitly.
-  No command requires fzf when BUNDLE is given, and okf bundles never uses it.
+  Giving BUNDLE bypasses its picker; profile and concept pickers are separate.
+  okf bundles never uses fzf.
 
   fzf reads from the terminal device, not standard input, so a menu still works
   inside a pipeline such as 'okf validate | less'.
@@ -89,3 +90,14 @@
 
   After selection, each command retains its existing success and failure
   semantics.
+
+THE KIT MENU
+
+  okf kit install                      Choose a skill or subagent with fzf.
+  okf kit install --project --tool-only Choose and install for this project.
+
+  Explicit NAME bypasses terminal and fzf probes. With NAME omitted, both are
+  required before the kit repository is refreshed. Unavailable selection exits
+  2 with a named-install alternative; an empty kit exits 1. Kit cancellation
+  exits 0 without installation; other Kit picker errors exit 1. The exit-status
+  table above describes bundle, profile and concept selection. See 'okf help kit'.
diff --git a/help/kit.md b/help/kit.md
--- a/help/kit.md
+++ b/help/kit.md
@@ -2,7 +2,7 @@
 
 okf kit installs reusable AI-agent skills and subagents from the configured
 okf-kit git repository. Installed items are made available to interactive agent
-sessions launched with okf assist.
+sessions launched with okf agent assist (also spelled okf assist).
 
 COMMANDS
 
@@ -26,6 +26,18 @@
   directory. Use project scope when a repository should carry its own agent
   helpers independently of your personal setup.
 
+VISIBILITY AND INSPECTION
+
+  list, status and update accept --json for versioned machine-readable output.
+  Installation uses the manifest's visibility, defaulting to tool-only. Use
+  --shared for ordinary provider sessions or --tool-only for sessions launched
+  by OKF. Codex subagents require shared visibility or --accept-shared-codex.
+  status reports requested and effective visibility, including repair needs.
+
+  Project operations and agent launches locate the same nearest ancestor with
+  .git, okf-config.dhall or .okf, falling back to the working directory. This
+  keeps project installations discoverable when launched from a subdirectory.
+
 CONFIGURATION
 
   The kit repository and target providers come from okf configuration:
@@ -42,3 +54,15 @@
   Skills are directories with a SKILL.md file. Subagents are Markdown persona
   files. Add an entry to kit.json, commit, push, and run 'okf kit update' to
   refresh local installs. No okf rebuild is required.
+
+INTERACTIVE INSTALLATION
+
+  okf kit install                     Choose a skill or subagent with fzf.
+  okf kit install --project --tool-only
+                                      Choose and install at project scope.
+
+  Named installation never probes fzf or a terminal. Omitted NAME requires both
+  fzf and a controlling terminal before repository refresh, or exits 2 with an
+  explicit-name alternative. An empty kit exits 1. Esc/Ctrl-C returns success
+  without installing anything; other picker failures exit 1. Name, kind and
+  description are displayed, and selection retains original manifest identity.
diff --git a/help/okf.md b/help/okf.md
--- a/help/okf.md
+++ b/help/okf.md
@@ -63,13 +63,16 @@
   alias         List configured command shortcuts.
   config        Show and manage okf configuration.
   kit           Install and manage agent skills and subagents.
-  assist        Launch an interactive agent session with installed okf skills.
+  agent         Inspect local OKF context or launch a contextual agent session.
+  assist        Alias for agent assist; a starting prompt is optional.
   completions   Generate a shell completion script.
 
 STANDALONE BY DESIGN
 
-  The okf CLI works on plain files only. It does not require Mori, Mina, an
-  LLM, PostgreSQL, or network access. It tracks Google's Open Knowledge Format
+  Local bundle commands work on plain files without Mori, Mina, an LLM,
+  PostgreSQL, or network access. Remote profile imports and Kit refreshes need
+  the network; agent launches require the selected provider CLI. okf tracks
+  Google's Open Knowledge Format
   v0.2 specification (the knowledge-catalog okf SPEC.md).
 
   It reads a v0.1 bundle unchanged. A bundle that declares nothing is read
@@ -91,9 +94,16 @@
   okf help where         The --where condition language for okf concepts.
   okf help ids           Profile-declared document IDs such as ADR-7.
   okf help interactive   Picking a bundle and concept with fzf.
-  okf help aliases      Command shortcuts and expansion rules.
+  okf help aliases       Command shortcuts and expansion rules.
   okf help config        Config files, precedence, and agent settings.
   okf help kit           Installing and publishing agent skills and subagents.
   okf help agents        Installing agent skills and launching assist.
 
   The full user guide lives under docs/user/ in the okf repository.
+
+HELP LAYOUT
+
+  okf help agents --width 60   Wrap prose at an explicit positive width.
+  Help automatically uses terminal width, capped at 140 columns. Indented
+  examples, fenced code and URLs are preserved. Piped default output keeps its
+  historical bytes. Bare 'okf help' lists topics; topic names support completion.
diff --git a/help/profiles.md b/help/profiles.md
--- a/help/profiles.md
+++ b/help/profiles.md
@@ -378,9 +378,41 @@
     profile: requests/example: missing profile-required field: reviews[2].outcome
     profile: requests/example: frontmatter element at reviews[1] must be a record, found: "not-a-record"
 
-  Extra keys inside a record remain allowed. Nested field-name closure and a
-  second nested level are not part of this schema.
+  NestedRules defaults to allowUnknownFields = True. To close either shape:
 
+    okf.defaults.NestedRules::{ allowUnknownFields = False, optional = [ ... ] }
+
+  Put those rules in elementFields for list records or objectFields for one
+  mapping. Every supplied undeclared key is reported, including null or empty
+  values; core keys such as title and the document-ID key have no exemption:
+
+    profile: note: nested field not declared by profile: rows[0].stray
+    profile: note: nested field not declared by profile: settings.extra
+
+  All names in required/recommended/optional count as declared even when a
+  presence condition is false. Optional rules still cannot have when clauses.
+  Profile/type names merge by union; allowUnknownFields merges with AND, with
+  an absent nested rule as identity. Either scope can close a shape and neither
+  can reopen it. List and object policies remain independent, including on a
+  field accepting both shapes. Its member and relation rules retain their
+  shared compiled contract. A closed empty member map rejects every key.
+
+  Closure applies with and without --strict and is advisory unless
+  --profile-enforce is supplied. Missing/empty parents retain presence checks,
+  and non-record list elements retain their shape diagnostic. No second nested
+  level is supported.
+
+  Human profile show and --json show declared allowUnknownFields at both
+  scopes. Generated documentation shows effective open/closed/none policies
+  alongside the merged declared names, including empty member maps.
+
+  Old descriptor pins keep open nested records through compatibility decoding.
+  Literals retyped against the current schema must add allowUnknownFields or
+  use NestedRules::{ ... } record completion; defaults do not repair existing
+  literals. Library consumers matching ProfileViolation must add a case for
+  NestedFieldNotInProfile ConceptId FieldPath, including
+  mori://shinzui/mori/packages/mori-cli. FieldNotInProfile remains top-level.
+
 RECORD-LIST UNIQUENESS
 
   A FieldRule with elementFields may set uniqueBy to the name of one nested
@@ -528,7 +560,7 @@
   runs are offline. Pass --registry with a local checkout to be offline
   throughout.
 
-  The built-in pin currently targets v0.19.0 and publishes seventeen OKF 0.2 profiles
+  The built-in pin currently targets v0.25.0 and publishes thirty OKF 0.2 profiles
   with descriptions. `okf profile sources` reports that version without network
   access. Pass --check-latest to that command for an explicit upstream tag
   comparison; a failed optional check is reported but does not change the source
@@ -569,3 +601,50 @@
   The full descriptor schema, and the upgrade steps above in detail, are
   documented in docs/user/profiles.md, whose "Generating profile
   documentation" section covers `okf profile document` at length.
+
+SAME-CONCEPT CONSTRAINTS
+
+  Profiles can constrain several values within one concept's frontmatter.
+  ValueSelector names a declared field and optionally one flat record member;
+  names cannot contain dots or brackets. No recursive or external resolution is
+  performed. Root selectors can project a member across record-list elements.
+
+  presenceConstraints combine an optional closed-vocabulary when gate,
+  existential whenAnyPresent triggers, and sibling requireAll/requireAny targets.
+  Empty triggers mean no presence gate; at least one target is required. Blank
+  strings and empty containers are absent; zero with scalar cardinality is
+  present. A projected root trigger can require a root sibling such as sources.
+
+  timestampOrderConstraints require distinct scalar Rfc3339Utc siblings with
+  before strictly earlier than after. Equal instants fail. Missing or malformed
+  endpoints retain ordinary field checks without an additional ordering error.
+
+  membershipConstraints require each source text occurrence to appear in the
+  target. uniqueValuesConstraints reject repeats across at least two distinct
+  selectors, including repeats within one selected list. Operands must be
+  declared scalar/list text leaves; supplied nontext values have indexed errors.
+  Text comparison is exact and case-sensitive, within this one concept.
+
+  FrontmatterRules has all four lists; NestedRules has presence and timestamp
+  lists, reading siblings in each record. Profile and type constraints accumulate.
+  All checks run in both modes and remain advisory without --profile-enforce.
+  Invalid definitions fail compilation before any bundle is validated.
+
+  FiniteDecimal accepts YAML numbers, including zero, integers and fractions,
+  without floating-point conversion. It rejects quoted numbers, booleans and
+  non-finite values and refines unspecified cardinality to scalar.
+
+  Use okf.defaults.FrontmatterRules::{ ... } and
+  okf.defaults.NestedRules::{ ... } to inherit empty constraint lists and open
+  nested records. Literals against the current schema need all new members;
+  older pinned descriptor shapes keep loading through compatibility decoders.
+  profile show --json exposes declarations; profile document shows effective
+  constraints for each type and nested shape.
+
+  Runnable fixtures from the repository root:
+
+    okf validate okf-core/test/fixtures/profile-product-discovery-valid --profile okf-core/test/fixtures/profiles/product-discovery.dhall --profile-enforce
+    okf profile show --registry okf-core/test/fixtures/profiles/product-discovery.dhall --no-local --json
+
+  The complete selector schema, migration guidance, and fixture walkthrough
+  are in docs/user/profiles.md and docs/user/fixtures.md.
diff --git a/help/validation.md b/help/validation.md
--- a/help/validation.md
+++ b/help/validation.md
@@ -125,3 +125,20 @@
   okf help computations  The attested computation checks, in context.
   okf help log           Log advisories and --log-enforce.
   okf help index         Declaring okf_version with okf index.
+
+PROFILE RELATIONS
+
+  Profile constraints can require grouped presence, order timestamps, check
+  text membership, and reject duplicate text across fields in one concept.
+  They run offline; indexed paths identify invalid occurrences. All profile
+  deviations remain advisory unless --profile-enforce is supplied.
+
+NESTED FIELD NAMES
+
+  A profile may close elementFields or objectFields with NestedRules'
+  allowUnknownFields = False. Each undeclared member produces a diagnostic at
+  its actual path, such as rows[0].stray or settings.extra. This includes keys
+  with null or empty values and core names such as title. Declared conditional
+  members stay declared when their presence condition is false. Both validation
+  modes check closure; --profile-enforce makes the advisory deviations fail.
+  Existing descriptors stay open unless they opt in.
diff --git a/okf-cli.cabal b/okf-cli.cabal
--- a/okf-cli.cabal
+++ b/okf-cli.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.4
 name: okf-cli
-version: 0.10.0.0
+version: 0.11.0.0
 synopsis: Command-line interface for Open Knowledge Format bundles
 description:
   okf-cli provides the @okf@ executable for working with Open Knowledge Format
@@ -46,9 +46,11 @@
   exposed-modules:
     Okf.Cli
     Okf.Cli.Agent.Config
+    Okf.Cli.Agent.Context
     Okf.Cli.Aliases
     Okf.Cli.Assist
     Okf.Cli.BundleDiscovery
+    Okf.Cli.CommandMetadata
     Okf.Cli.Completions
     Okf.Cli.Config
     Okf.Cli.Fzf
@@ -56,6 +58,7 @@
     Okf.Cli.Help
     Okf.Cli.Kit
     Okf.Cli.Kit.Config
+    Okf.Cli.Kit.Selector
     Okf.Cli.ProfileDiscovery
     Okf.Cli.Version
 
@@ -63,11 +66,11 @@
   autogen-modules: Paths_okf_cli
   build-depends:
     aeson >=2.2 && <2.4,
-    baikai ^>=0.7.2.0,
-    baikai-claude ^>=0.7.1.0,
-    baikai-kit ^>=0.4.0.0,
-    baikai-openai ^>=0.7.1.0,
-    base >=4.20 && <5,
+    baikai ^>=0.10.0.0,
+    baikai-claude ^>=0.7.3.0,
+    baikai-kit ^>=0.4.0.3,
+    baikai-openai ^>=0.7.3.0,
+    base >=4.21 && <5,
     bytestring >=0.11 && <0.13,
     containers >=0.6 && <0.8,
     dhall >=1.41 && <1.43,
@@ -77,9 +80,10 @@
     generic-lens >=2.2 && <2.4,
     githash ^>=0.1,
     lens ^>=5.3,
-    okf-core ^>=0.10.0.0,
-    optparse-applicative >=0.18 && <0.20,
+    okf-core ^>=0.11.0.0,
+    optparse-applicative >=0.19 && <0.20,
     process >=1.6 && <1.7,
+    terminal-size >=0.3.4 && <0.4,
     text ^>=2.1,
     time >=1.12 && <1.15,
 
@@ -90,14 +94,19 @@
   hs-source-dirs: test
   build-depends:
     aeson >=2.2 && <2.4,
-    baikai-kit ^>=0.4.0.0,
-    base >=4.20 && <5,
+    baikai ^>=0.10.0.0,
+    baikai-claude ^>=0.7.3.0,
+    baikai-kit ^>=0.4.0.3,
+    baikai-openai ^>=0.7.3.0,
+    base >=4.21 && <5,
     containers >=0.6 && <0.8,
     directory,
     filepath,
+    generic-lens >=2.2 && <2.4,
+    lens ^>=5.3,
     okf-cli,
-    okf-core ^>=0.10.0.0,
-    optparse-applicative >=0.18,
+    okf-core ^>=0.11.0.0,
+    optparse-applicative >=0.19,
     temporary,
     text ^>=2.1,
     time >=1.12 && <1.15,
@@ -112,5 +121,5 @@
     -with-rtsopts=-N
 
   build-depends:
-    base >=4.20 && <5,
+    base >=4.21 && <5,
     okf-cli,
diff --git a/src/Okf/Cli.hs b/src/Okf/Cli.hs
--- a/src/Okf/Cli.hs
+++ b/src/Okf/Cli.hs
@@ -72,6 +72,7 @@
 import Data.ByteString.Lazy.Char8 qualified as LazyByteString
 import Data.Char (isDigit)
 import Data.Foldable (toList, traverse_)
+import Data.Generics.Labels ()
 import Data.List qualified as List
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Map.Strict qualified as Map
@@ -96,9 +97,11 @@
     renderAgentResolution,
     resolveAgent,
   )
+import Okf.Cli.Agent.Context (collectAgentContext, renderContextJson, renderContextMarkdown)
 import Okf.Cli.Aliases (AliasCommand (..), aliasCommandParser, expandAlias, isAliasCandidate, renderAliases)
 import Okf.Cli.Assist (AssistOptions, assistAgentOverrides, assistOptionsParser, handleAssistCommand)
 import Okf.Cli.BundleDiscovery (BundleDiscovery (..), discoverAvailableBundles)
+import Okf.Cli.CommandMetadata (commandDescription, commandExamples)
 import Okf.Cli.Completions (CompletionsShell, completionsParser, handleCompletions)
 import Okf.Cli.Config
 import Okf.Cli.Fzf (FzfConfig, detectFzfConfig, shellQuote)
@@ -143,6 +146,7 @@
 import Okf.Index
 import Okf.Log qualified as Log
 import Okf.Path (PathReference (..), classifyPathReference)
+
 -- 'List' and 'Object' are 'Okf.Profile.Cardinality' constructors here; aeson's
 -- same-named 'Value' constructors are reached as 'Aeson.Object' and friends.
 import Okf.Prelude hiding (List, Object)
@@ -170,11 +174,15 @@
     fieldRuleDescription,
     fieldRuleElementFields,
     fieldRuleObjectFields,
+    frontmatterConstraints,
     loadProfileFile,
+    nestedConstraints,
     nextDocumentId,
     parseDocumentId,
     profileFieldDescriptionForType,
+    renderConceptConstraints,
     renderDocumentId,
+    renderValueSelector,
     validateProfileVersion,
     validateProfileWith,
   )
@@ -263,6 +271,7 @@
   | Config ConfigCommand
   | Profile ProfileCommand
   | Kit KitCommand
+  | AgentContext Bool
   | Assist AssistOptions
   | Completions CompletionsShell
   | Help HelpCommand
@@ -487,7 +496,7 @@
   rawArgs <- getArgs
   aliases <- if isAliasCandidate builtinCommands rawArgs then loadAliasesForExpansion else pure Map.empty
   let expandedArgs = expandAlias builtinCommands aliases rawArgs
-  Options {cmd} <- handleParseResult (execParserPure defaultPrefs parserInfo expandedArgs)
+  Options {cmd} <- handleParseResult (execParserPure (prefs (showHelpOnEmpty <> showHelpOnError)) parserInfo expandedArgs)
   runCommand cmd
 
 parserInfo :: ParserInfo Options
@@ -497,6 +506,7 @@
     ( fullDesc
         <> progDesc "Validate, index, inspect, and graph Open Knowledge Format bundles"
         <> header "okf - Open Knowledge Format bundle tools"
+        <> footer (commandExamples ["bundles", "agent", "kit", "help"])
     )
 
 versionOption :: Parser (a -> a)
@@ -511,25 +521,26 @@
 -- | One registry owns parser registration, help order, and alias protection.
 commandDefinitions :: [(String, ParserInfo Command)]
 commandDefinitions =
-  [ ("bundles", info (Bundles <$> bundlesOptionsParser <**> helper) (progDesc "List discovered OKF bundles")),
-    ("profiles", info (Profiles <$> profilesOptionsParser <**> helper) (progDesc "List local profile descriptor paths; use `okf profile list` for all effective sources")),
-    ("validate", info (Validate <$> validateOptionsParser <**> helper) (progDesc "Validate an OKF bundle")),
-    ("index", info (Index <$> indexOptionsParser <**> helper) (progDesc "Preview or write generated index.md files")),
-    ("log", info (Log <$> logOptionsParser <**> helper) (progDesc "Preview and check log.md files")),
-    ("graph", info (GraphCommand <$> graphOptionsParser <**> helper) (progDesc "Print a bundle graph")),
-    ("show", info (ShowConcept <$> showOptionsParser <**> helper) (progDesc "Show one concept")),
-    ("trust", info (Trust <$> trustOptionsParser <**> helper) (progDesc "Report trust tiers, status, and staleness for every concept")),
-    ("sources", info (Sources <$> sourcesOptionsParser <**> helper) (progDesc "List the provenance recorded by each concept")),
-    ("computations", info (Computations <$> computationsOptionsParser <**> helper) (progDesc "List the attested computations a bundle declares")),
-    ("concepts", info (Concepts <$> conceptsOptionsParser <**> helper) (progDesc "List the concepts a bundle holds, with optional filters")),
-    ("id", info (Id <$> idOptionsParser <**> helper) (progDesc "Allocate and list document IDs")),
-    ("alias", info (Alias <$> aliasCommandParser <**> helper) (progDesc "List configured command aliases")),
-    ("config", info (Config <$> configCommandParser <**> helper) (progDesc "Show and manage okf configuration")),
-    ("profile", info (Profile <$> profileCommandParser <**> helper) (progDesc "List and inspect profiles from registries and local descriptors")),
-    ("kit", info (Kit <$> kitCommandParser <**> helper) (progDesc "Install and manage agent skills and subagents")),
-    ("assist", info (Assist <$> assistOptionsParser <**> helper) (progDesc "Launch an interactive agent session with installed okf skills")),
-    ("completions", info (Completions <$> completionsParser <**> helper) (progDesc "Generate a shell completion script (bash, zsh, fish)")),
-    ("help", info (Help <$> helpCommandParser <**> helper) (progDesc "Show conceptual help topics"))
+  [ ("bundles", info (Bundles <$> bundlesOptionsParser <**> helper) (progDesc (commandDescription "bundles"))),
+    ("profiles", info (Profiles <$> profilesOptionsParser <**> helper) (progDesc (commandDescription "profiles"))),
+    ("validate", info (Validate <$> validateOptionsParser <**> helper) (progDesc (commandDescription "validate"))),
+    ("index", info (Index <$> indexOptionsParser <**> helper) (progDesc (commandDescription "index"))),
+    ("log", info (Log <$> logOptionsParser <**> helper) (progDesc (commandDescription "log"))),
+    ("graph", info (GraphCommand <$> graphOptionsParser <**> helper) (progDesc (commandDescription "graph"))),
+    ("show", info (ShowConcept <$> showOptionsParser <**> helper) (progDesc (commandDescription "show"))),
+    ("trust", info (Trust <$> trustOptionsParser <**> helper) (progDesc (commandDescription "trust"))),
+    ("sources", info (Sources <$> sourcesOptionsParser <**> helper) (progDesc (commandDescription "sources"))),
+    ("computations", info (Computations <$> computationsOptionsParser <**> helper) (progDesc (commandDescription "computations"))),
+    ("concepts", info (Concepts <$> conceptsOptionsParser <**> helper) (progDesc (commandDescription "concepts"))),
+    ("id", info (Id <$> idOptionsParser <**> helper) (progDesc (commandDescription "id"))),
+    ("alias", info (Alias <$> aliasCommandParser <**> helper) (progDesc (commandDescription "alias"))),
+    ("config", info (Config <$> configCommandParser <**> helper) (progDesc (commandDescription "config"))),
+    ("profile", info (Profile <$> profileCommandParser <**> helper) (progDesc (commandDescription "profile"))),
+    ("kit", info (Kit <$> kitCommandParser <**> helper) (progDesc (commandDescription "kit") <> footer (commandExamples ["kit"]))),
+    ("agent", info (agentCommandParser <**> helper) (progDesc (commandDescription "agent") <> footer (commandExamples ["agent"]))),
+    ("assist", info (Assist <$> assistOptionsParser <**> helper) (progDesc (commandDescription "assist") <> footer (commandExamples ["assist"]))),
+    ("completions", info (Completions <$> completionsParser <**> helper) (progDesc (commandDescription "completions"))),
+    ("help", info (Help <$> helpCommandParser <**> helper) (progDesc (commandDescription "help")))
   ]
 
 builtinCommands :: [Text]
@@ -538,6 +549,13 @@
 commandParser :: Parser Command
 commandParser = hsubparser (foldMap (uncurry command) commandDefinitions)
 
+agentCommandParser :: Parser Command
+agentCommandParser =
+  hsubparser
+    ( command "assist" (info (Assist <$> assistOptionsParser <**> helper) (progDesc (commandDescription "agent assist") <> footer (commandExamples ["agent assist"])))
+        <> command "context" (info (AgentContext <$> parserOptionGroup "Context output" jsonSwitch <**> helper) (progDesc (commandDescription "agent context") <> footer (commandExamples ["agent context"])))
+    )
+
 bundlesOptionsParser :: Parser BundlesOptions
 bundlesOptionsParser = BundlesOptions <$> jsonSwitch
 
@@ -981,6 +999,11 @@
   Kit kitCommand -> do
     config <- loadConfigOrDie
     handleKitCommand config kitCommand
+  AgentContext asJson -> do
+    context <- collectAgentContext
+    if asJson
+      then LazyByteString.putStrLn (renderContextJson context)
+      else Text.IO.putStr (renderContextMarkdown context)
   Assist assistOptions -> do
     config <- loadConfigOrDie
     resolved <- resolveAgentOrDie AgentCmdAssist (assistAgentOverrides assistOptions)
@@ -2047,6 +2070,7 @@
         renderFieldRules indent "frontmatter.required" (rules ^. #required)
           <> renderFieldRules indent "frontmatter.recommended" (rules ^. #recommended)
           <> renderFieldRules indent "frontmatter.optional" (rules ^. #optional)
+          <> map (indent <>) (renderConceptConstraints (frontmatterConstraints rules))
 
       -- A field's prose cannot share a comma-joined line with its neighbours, so
       -- a non-empty list becomes a headed block. An empty list keeps the
@@ -2079,10 +2103,11 @@
       renderNestedBlock indent label = \case
         Nothing -> [indent <> "    " <> label <> ": (none)"]
         Just nestedRules ->
-          [indent <> "    " <> label <> ":"]
+          [indent <> "    " <> label <> ":", indent <> "      allowUnknownFields: " <> (if nestedRules ^. #allowUnknownFields then "true" else "false")]
             <> renderNestedFieldRules (indent <> "      ") "required" (nestedRules ^. #required)
             <> renderNestedFieldRules (indent <> "      ") "recommended" (nestedRules ^. #recommended)
             <> renderNestedFieldRules (indent <> "      ") "optional" (nestedRules ^. #optional)
+            <> map ((indent <> "      ") <>) (take 2 (renderConceptConstraints (nestedConstraints nestedRules)))
 
       renderNestedFieldRules indent label [] = [indent <> label <> ": " <> renderList []]
       renderNestedFieldRules indent label rules =
@@ -3131,6 +3156,12 @@
 -- compiled profile and an empty list.
 renderProfileViolation :: CompiledProfile -> [Concept] -> ProfileViolation -> Text
 renderProfileViolation compiled concepts = \case
+  ConstraintPresenceMissing cid path gate -> renderConceptId cid <> ": constraint requires " <> renderFieldPath path <> renderConditionContext gate
+  ConstraintAlternativeMissing cid paths gate -> renderConceptId cid <> ": constraint requires any of " <> Text.intercalate ", " (map renderFieldPath paths) <> renderConditionContext gate
+  TimestampOrderViolation cid before after -> renderConceptId cid <> ": timestamp " <> renderFieldPath before <> " must precede " <> renderFieldPath after
+  TextOperandMismatch cid path actual -> renderConceptId cid <> ": relation operand at " <> renderFieldPath path <> " must be text, found: " <> renderJsonValue actual
+  MembershipValueMissing cid path value target -> renderConceptId cid <> ": value at " <> renderFieldPath path <> " is not in " <> renderValueSelector target <> ": " <> value
+  DuplicateSelectedValue cid path value original -> renderConceptId cid <> ": duplicate value at " <> renderFieldPath path <> " (first at " <> renderFieldPath original <> "): " <> value
   TypeNotInProfile cid ctype ->
     renderConceptId cid <> ": type not in profile vocabulary: " <> ctype
   MissingProfileField cid key condition ->
@@ -3255,6 +3286,8 @@
       <> renderFieldPath fieldPath
       <> " climbs above the bundle root: "
       <> rawPath
+  NestedFieldNotInProfile cid fieldPath ->
+    renderConceptId cid <> ": nested field not declared by profile: " <> renderFieldPath fieldPath
   FieldNotInProfile cid key ->
     renderConceptId cid <> ": frontmatter field not declared by profile: " <> key
   NestedElementNotRecord cid fieldPath actual ->
@@ -3341,6 +3374,7 @@
 
 renderProfileDefinitionError :: ProfileDefinitionError -> Text
 renderProfileDefinitionError = \case
+  InvalidConceptConstraint scope parent sel reason -> renderScope scope <> ": invalid constraint " <> maybe "" (<> ".") parent <> renderValueSelector sel <> ": " <> reason
   DuplicateTypeRule ctype -> "duplicate type rule: " <> ctype
   DuplicateFieldRule scope listName key ->
     renderScope scope <> ": duplicate " <> listName <> " field: " <> key
@@ -3555,6 +3589,7 @@
   HumanActor -> "human-actor"
   Integer -> "integer"
   NonNegativeInteger -> "non-negative-integer"
+  FiniteDecimal -> "finite-decimal"
   Boolean -> "boolean"
 
 renderHandleReferenceRule :: HandleReferenceRule -> Text
diff --git a/src/Okf/Cli/Agent/Context.hs b/src/Okf/Cli/Agent/Context.hs
new file mode 100644
--- /dev/null
+++ b/src/Okf/Cli/Agent/Context.hs
@@ -0,0 +1,185 @@
+-- | Read-only local context collection, with deterministic pure renderers.
+-- No registry refresh, config evaluation, provider invocation or clock access.
+module Okf.Cli.Agent.Context
+  ( AgentContext (..),
+    ProfileContext (..),
+    AssetContext (..),
+    collectAgentContext,
+    renderContextMarkdown,
+    renderContextJson,
+  )
+where
+
+import Baikai.Interactive (InteractiveProvider (..))
+import Baikai.Kit.Config qualified as Kit
+import Baikai.Kit.Manifest (kindLabel)
+import Baikai.Kit.Sidecar (sidecarPath)
+import Baikai.Kit.Status (InstalledCopy (..), installedCopies)
+import Control.Exception (IOException, try)
+import Control.Lens ((&), (.~), (^.))
+import Control.Monad (filterM)
+import Data.Aeson (ToJSON, encode)
+import Data.ByteString.Lazy (ByteString)
+import Data.Generics.Labels ()
+import Data.List (nub, sort, sortOn)
+import Data.Text (Text)
+import Data.Text qualified as Text
+import GHC.Generics (Generic)
+import Okf.Cli.BundleDiscovery (bundleSearchRoots)
+import Okf.Cli.CommandMetadata (CommandGuide (..), commandGuides)
+import Okf.Cli.Config (defaultOkfConfig)
+import Okf.Cli.Kit.Config (kitConfig)
+import Okf.Cli.ProfileDiscovery (profileSearchRoots)
+import Okf.Discovery (defaultDiscoveryOptions, discoverBundleRoots)
+import Okf.Profile (ProfileSpec, TypeRule)
+import Okf.Profile.Discovery (defaultProfileDiscoveryOptions, discoverProfileDescriptors, loadProfileDescriptorWithoutNetwork)
+import System.Directory (canonicalizePath, doesDirectoryExist, doesFileExist, getCurrentDirectory, getPermissions, readable)
+
+-- | Version 1 uses absolute paths throughout, independent of invocation depth.
+data AgentContext = AgentContext
+  { schemaVersion :: !Int,
+    workingDirectory :: !FilePath,
+    projectRoot :: !FilePath,
+    bundles :: ![FilePath],
+    profiles :: ![ProfileContext],
+    commands :: ![CommandGuide],
+    installedAssets :: ![AssetContext],
+    warnings :: ![Text]
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (ToJSON)
+
+data ProfileContext = ProfileContext
+  { path :: !FilePath,
+    name :: !Text,
+    description :: !(Maybe Text),
+    guidance :: !(Maybe Text),
+    typeGuidance :: ![(Text, Text)]
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (ToJSON)
+
+data AssetContext = AssetContext
+  { path :: !FilePath,
+    name :: !Text,
+    kind :: !Text,
+    provider :: !Text,
+    scope :: !Text
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (ToJSON)
+
+collectAgentContext :: IO AgentContext
+collectAgentContext = do
+  workingDirectory <- getCurrentDirectory
+  -- Configuration cannot change the tool identity or root policy. Inspect both
+  -- providers without evaluating potentially remote/secret-bearing Dhall config.
+  let kit = kitConfig defaultOkfConfig & #providers .~ [InteractiveClaude, InteractiveCodex]
+  projectRoot <- kit ^. #projectRoot
+  bundleRoots <- rootsAt projectRoot <$> bundleSearchRoots
+  profileRoots <- rootsAt projectRoot <$> profileSearchRoots
+  (bundlePaths, bundleWarnings) <- discover "bundles" (discoverBundleRoots defaultDiscoveryOptions) bundleRoots
+  (profilePaths, profileWarnings) <- discover "profiles" (discoverProfileDescriptors defaultProfileDiscoveryOptions) profileRoots
+  loadedProfiles <- traverse loadProfile profilePaths
+  (assets, assetWarnings) <- optionalSource "installed kit assets" [] $ do
+    copies <- installedCopies kit >>= filterM (ownedCopy kit)
+    traverse assetContext copies
+  pure
+    AgentContext
+      { schemaVersion = 1,
+        workingDirectory,
+        projectRoot,
+        bundles = sort (nub bundlePaths),
+        profiles = sortOn (\ProfileContext {path} -> path) [p | (Just p, _) <- loadedProfiles],
+        commands = sortOn (\CommandGuide {commandPath} -> commandPath) commandGuides,
+        installedAssets = sortOn (\AssetContext {path, provider, scope} -> (path, provider, scope)) assets,
+        warnings = sort (nub (bundleWarnings <> profileWarnings <> concatMap snd loadedProfiles <> assetWarnings))
+      }
+  where
+    rootsAt root ["."] = [root]
+    rootsAt _ roots = roots
+    ownedCopy kit InstalledCopy {name, kind, scope, provider} = do
+      base <- Kit.providerAgentsBase kit provider scope
+      doesFileExist (sidecarPath provider kind name base (Kit.sidecarFileName kit))
+    assetContext InstalledCopy {path, name, kind, provider, scope} = do
+      absolute <- canonicalizePath path
+      pure AssetContext {path = absolute, name, kind = kindLabel kind, provider = Kit.providerLabel provider, scope = Kit.scopeLabel scope}
+
+-- Expected IO failures are warnings; cancellation is never converted to data.
+optionalSource :: Text -> a -> IO a -> IO (a, [Text])
+optionalSource label fallback action = do
+  result <- try @IOException action
+  pure $ case result of
+    Left _ -> (fallback, [label <> " is inaccessible; inspect it directly for diagnostics."])
+    Right value -> (value, [])
+
+discover :: Text -> (FilePath -> IO [FilePath]) -> [FilePath] -> IO ([FilePath], [Text])
+discover label scanner roots = do
+  results <- traverse scan (sort (nub roots))
+  pure (concatMap fst results, concatMap snd results)
+  where
+    scan root = optionalSource (label <> " source " <> Text.pack root) [] $ do
+      exists <- doesDirectoryExist root
+      if not exists then ioError (userError "missing directory") else pure ()
+      permissions <- getPermissions root
+      if not (readable permissions) then ioError (userError "unreadable directory") else pure ()
+      scanner root >>= traverse canonicalizePath
+
+loadProfile :: FilePath -> IO (Maybe ProfileContext, [Text])
+loadProfile path = do
+  loaded <- loadProfileDescriptorWithoutNetwork path
+  pure $ case loaded of
+    Left _ -> (Nothing, ["Profile became unavailable: " <> Text.pack path])
+    Right (spec :: ProfileSpec) ->
+      ( Just
+          ProfileContext
+            { path,
+              name = spec ^. #name,
+              description = spec ^. #description,
+              guidance = spec ^. #guidance,
+              typeGuidance = sort [(rule ^. #type_, text) | (rule :: TypeRule) <- spec ^. #types, Just text <- [rule ^. #guidance]]
+            },
+        []
+      )
+
+renderContextJson :: AgentContext -> ByteString
+renderContextJson = encode
+
+renderContextMarkdown :: AgentContext -> Text
+renderContextMarkdown AgentContext {workingDirectory, projectRoot, bundles, profiles, commands, installedAssets, warnings} =
+  Text.unlines $
+    [ "# OKF project context",
+      "Local discovery is read-only and bounded to depth four; no registry was refreshed.",
+      "This is context, not a user task. Wait for the user's instructions when no task was supplied.",
+      "Working directory: " <> Text.pack workingDirectory,
+      "Project root: " <> Text.pack projectRoot,
+      "All discovered paths below are absolute. Command example paths are relative to the working directory.",
+      "",
+      "## Bundles"
+    ]
+      <> nonempty "No bundles discovered; use `okf bundles --json` or set OKF_BUNDLE_ROOTS." (map Text.pack bundles)
+      <> ["", "## Local profiles"]
+      <> nonempty "No local profiles discovered; use `okf profiles --json` or `okf profile list`." (concatMap profileLines profiles)
+      <> ["", "## Installed OKF kit assets"]
+      <> nonempty "No OKF kit assets discovered; use `okf kit list` and `okf kit install NAME`." (map assetLine installedAssets)
+      <> [ "",
+           "## Commands",
+           "Examples use placeholders BUNDLE, CONCEPT, PROFILE, EXPORT and NAME; replace them with discovered values.",
+           "Commands marked 'can write' have writing subcommands or flags; inspect --help and preview before writing."
+         ]
+      <> concatMap commandLines commands
+      <> ["", "## Discovery warnings"]
+      <> nonempty "None." warnings
+  where
+    nonempty fallback [] = [fallback]
+    nonempty _ values = map ("- " <>) values
+    profileLines ProfileContext {path, name, description, guidance, typeGuidance} =
+      [name <> " — " <> Text.pack path]
+        <> maybe [] pure description
+        <> maybe [] pure guidance
+        <> [typ <> ": " <> text | (typ, text) <- typeGuidance]
+    assetLine AssetContext {path, name, kind, provider, scope} =
+      name <> " (" <> kind <> ", " <> provider <> ", " <> scope <> ") — " <> Text.pack path
+    commandLines CommandGuide {commandPath, description, examples, writesFiles} =
+      ["- `okf " <> commandPath <> "`: " <> description <> if writesFiles then " (can write)" else " (read-only)"]
+        <> map (\example -> "  - `" <> example <> "`") examples
diff --git a/src/Okf/Cli/Assist.hs b/src/Okf/Cli/Assist.hs
--- a/src/Okf/Cli/Assist.hs
+++ b/src/Okf/Cli/Assist.hs
@@ -8,6 +8,7 @@
 -- that launches an agent.
 module Okf.Cli.Assist
   ( AssistOptions (..),
+    AssistInspection (..),
     assistOptionsParser,
     assistAgentOverrides,
     handleAssistCommand,
@@ -17,14 +18,15 @@
 where
 
 import Baikai.Agent (AgentRenderError, renderAgentRenderError)
-import Baikai.Interactive (InteractiveLaunchRequest, interactiveLaunchRequest)
+import Baikai.Interactive (InteractiveLaunchRequest, InteractiveProvider (..), interactiveLaunchRequest)
 import Baikai.Kit.Session (agentDirsForSession, codexSessionArgs)
 import Baikai.Provider.Claude.Interactive (claudeInteractiveCommand, defaultClaudeInteractiveConfig)
 import Baikai.Provider.OpenAI.Interactive (codexInteractiveCommand, defaultCodexInteractiveConfig)
-import Control.Exception (IOException, try)
+import Control.Exception (IOException, bracket, try)
 import Control.Lens ((%~), (&), (.~))
 import Data.Bifunctor (first)
 import Data.Generics.Labels ()
+import Data.Maybe (fromMaybe)
 import Data.Text (Text)
 import Data.Text qualified as Text
 import Data.Text.IO qualified as Text.IO
@@ -36,56 +38,79 @@
     parseOkfProvider,
     thinkingLevelOf,
   )
+import Okf.Cli.Agent.Context (collectAgentContext, renderContextMarkdown)
 import Okf.Cli.Config (OkfConfig, OkfEffort, OkfProvider (..))
+import Okf.Cli.Fzf (shellQuote)
 import Okf.Cli.Kit.Config (kitConfig)
 import Options.Applicative
 import System.Exit (ExitCode (..), exitWith)
-import System.IO (hPutStrLn, stderr)
-import System.Process (createProcess, delegate_ctlc, proc, waitForProcess)
+import System.IO (IOMode (ReadMode), hClose, hPutStrLn, openFile, stderr)
+import System.Process (delegate_ctlc, proc, waitForProcess, withCreateProcess)
 
+-- | Inspection modes are a parser alternative, so conflicting flags fail.
+data AssistInspection = LaunchSession | PrintCommand | DebugContext
+  deriving stock (Show, Eq)
+
 data AssistOptions = AssistOptions
-  { prompt :: !Text,
+  { prompt :: !(Maybe Text),
     providerOverride :: !(Maybe OkfProvider),
     modelOverride :: !(Maybe Text),
     effortOverride :: !(Maybe OkfEffort),
     systemPromptOverride :: !(Maybe Text),
-    printCommand :: !Bool
+    inspection :: !AssistInspection
   }
   deriving stock (Show, Eq)
 
 assistOptionsParser :: Parser AssistOptions
 assistOptionsParser =
   AssistOptions
-    <$> (Text.pack <$> strArgument (metavar "PROMPT" <> help "The task or question to start the agent session with"))
-    <*> optional
-      ( option
-          (eitherReader (first Text.unpack . parseOkfProvider . Text.pack))
-          ( long "provider"
-              <> metavar "PROVIDER"
-              <> help "Agent CLI to launch: claude or codex"
+    <$> optional (Text.pack <$> strArgument (metavar "PROMPT" <> help "Optional task or question to start the interactive session"))
+    <*> parserOptionGroup
+      "Agent settings"
+      ( optional
+          ( option
+              (eitherReader (first Text.unpack . parseOkfProvider . Text.pack))
+              ( long "provider"
+                  <> metavar "PROVIDER"
+                  <> help "Agent CLI to launch: claude or codex"
+              )
           )
       )
-    <*> optional
-      ( Text.pack
-          <$> strOption (long "model" <> metavar "MODEL" <> help "Override the assist model from config")
+    <*> parserOptionGroup
+      "Agent settings"
+      ( optional
+          ( Text.pack
+              <$> strOption (long "model" <> metavar "MODEL" <> help "Override the assist model from config")
+          )
       )
-    <*> optional
-      ( option
-          (eitherReader (first Text.unpack . parseOkfEffort . Text.pack))
-          ( long "effort"
-              <> metavar "LEVEL"
-              <> help "Reasoning effort: minimal, low, medium, high, xhigh, or max"
+    <*> parserOptionGroup
+      "Agent settings"
+      ( optional
+          ( option
+              (eitherReader (first Text.unpack . parseOkfEffort . Text.pack))
+              ( long "effort"
+                  <> metavar "LEVEL"
+                  <> help "Reasoning effort: minimal, low, medium, high, xhigh, or max"
+              )
           )
       )
-    <*> optional
-      ( Text.pack
-          <$> strOption
-            ( long "system-prompt"
-                <> metavar "TEXT"
-                <> help "Extra system prompt, appended to the agent's own"
-            )
+    <*> parserOptionGroup
+      "Agent settings"
+      ( optional
+          ( Text.pack
+              <$> strOption
+                ( long "system-prompt"
+                    <> metavar "TEXT"
+                    <> help "Extra system prompt, appended to the agent's own"
+                )
+          )
       )
-    <*> switch (long "print-command" <> help "Print the agent command line instead of launching it")
+    <*> parserOptionGroup
+      "Inspection"
+      ( flag' PrintCommand (long "print-command" <> help "Print the rendered interactive command without launching")
+          <|> flag' DebugContext (long "debug" <> help "Print the live OKF context without launching")
+          <|> pure LaunchSession
+      )
 
 -- | The flag layer of the precedence chain. Values are already parsed, so
 -- @--effort medum@ fails during argument parsing with the list of valid levels
@@ -125,7 +150,7 @@
       -- text through the request's own systemPrompt field would render
       -- --system-prompt, which /replaces/ Claude's system prompt; that would
       -- silently change how every existing assist session behaves. Keep
-      -- appending until Baikai can express the difference (baikai IR-4).
+      -- appending until Baikai can express the difference (mori://shinzui/baikai/okf/improvement-requests/concepts/IR-4).
       applySystemPrompt = \mSystemPrompt request ->
         request & #extraArgs .~ maybe [] (\text -> ["--append-system-prompt", text]) mSystemPrompt,
       buildCommand = claudeInteractiveCommand defaultClaudeInteractiveConfig
@@ -152,7 +177,7 @@
   agentDirs
   AssistOptions {prompt} =
     applySystemPrompt launcher (resolvedValue <$> systemPrompt) $
-      interactiveLaunchRequest prompt
+      interactiveLaunchRequest (fromMaybe "" prompt)
         & #modelId .~ (resolvedValue <$> model)
         & #effort .~ (thinkingLevelOf . resolvedValue <$> effort)
         & #extraDirs .~ agentDirs
@@ -178,28 +203,43 @@
   OkfConfig -> ResolvedAgent -> AssistOptions -> IO (Either AgentRenderError (FilePath, [String]))
 buildSessionAgentCommand config resolved options = do
   let launcher = launcherFor (resolvedProviderOf resolved)
-      kit = kitConfig config
+      kit = kitConfig config & #providers .~ [InteractiveClaude, InteractiveCodex]
   agentDirs <- agentDirsForSession kit
   sessionArgs <- case resolvedProviderOf resolved of
     ProviderClaude -> pure []
     ProviderCodex -> codexSessionArgs kit
-  let request =
-        assistLaunchRequest launcher resolved agentDirs options
+  context <- renderContextMarkdown <$> collectAgentContext
+  pure (buildContextualCommand launcher resolved agentDirs options sessionArgs context)
+
+buildContextualCommand :: InteractiveLauncher -> ResolvedAgent -> [FilePath] -> AssistOptions -> [Text] -> Text -> Either AgentRenderError (FilePath, [String])
+buildContextualCommand launcher resolved@ResolvedAgent {systemPrompt} agentDirs options sessionArgs context =
+  let extraInstructions = maybe [] (pure . resolvedValue) systemPrompt
+      instructions = Just (Text.intercalate "\n\n" (context : extraInstructions))
+      request =
+        applySystemPrompt launcher instructions (assistLaunchRequest launcher resolved agentDirs options)
           & #extraArgs %~ (<> sessionArgs)
-  pure (buildCommand launcher request)
+   in buildCommand launcher request
 
 handleAssistCommand :: OkfConfig -> ResolvedAgent -> AssistOptions -> IO ()
-handleAssistCommand config resolved options = do
-  let launcher = launcherFor (resolvedProviderOf resolved)
-  renderedCommand <- buildSessionAgentCommand config resolved options
-  case renderedCommand of
-    Left renderError -> do
-      Text.IO.hPutStrLn stderr ("okf assist: " <> renderAgentRenderError renderError)
-      exitWith (ExitFailure 2)
-    Right (executable, argv)
-      | printCommand options ->
-          Text.IO.putStrLn (Text.pack (unwords (executable : map quoteArg argv)))
-      | otherwise -> launchAgent launcher executable argv
+handleAssistCommand config resolved options@AssistOptions {inspection}
+  | inspection == DebugContext = collectAgentContext >>= Text.IO.putStr . renderContextMarkdown
+  | otherwise = do
+      let launcher = launcherFor (resolvedProviderOf resolved)
+      renderedCommand <- buildSessionAgentCommand config resolved options
+      case renderedCommand of
+        Left renderError -> do
+          Text.IO.hPutStrLn stderr ("okf agent assist: " <> renderAgentRenderError renderError)
+          exitWith (ExitFailure 2)
+        Right (executable, argv)
+          | inspection == PrintCommand ->
+              Text.IO.putStrLn (Text.unwords (map (shellQuote . Text.pack) (executable : argv)))
+          | otherwise -> do
+              terminal <- try @IOException (bracket (openFile "/dev/tty" ReadMode) hClose (const (pure ())))
+              case terminal of
+                Left _ -> do
+                  hPutStrLn stderr "okf agent assist: no controlling terminal; use --debug, --print-command, or `okf agent context --json`."
+                  exitWith (ExitFailure 2)
+                Right () -> launchAgent launcher executable argv
 
 -- | Spawn the rendered command and wait for the session to end.
 --
@@ -209,12 +249,12 @@
 -- @launchCodexInteractive@ calls 'createProcess' without it. Without
 -- delegation the terminal's SIGINT reaches okf as well as the agent, so the
 -- first Ctrl-C inside a session would kill okf and orphan the agent. See
--- baikai IR-5.
+-- mori://shinzui/baikai/okf/improvement-requests/concepts/IR-5.
 launchAgent :: InteractiveLauncher -> FilePath -> [String] -> IO ()
 launchAgent launcher executable argv = do
-  result <- try @IOException $ do
-    (_, _, _, processHandle) <- createProcess (proc executable argv) {delegate_ctlc = True}
-    waitForProcess processHandle
+  result <- try @IOException $
+    withCreateProcess (proc executable argv) {delegate_ctlc = True} $ \_ _ _ processHandle ->
+      waitForProcess processHandle
   case result of
     Left exception -> do
       hPutStrLn stderr $
@@ -227,13 +267,3 @@
           <> " or run `okf assist --print-command ...` to inspect the command."
       exitWith (ExitFailure 127)
     Right exitCode -> exitWith exitCode
-
-quoteArg :: String -> String
-quoteArg arg
-  | null arg = "''"
-  | any needsQuote arg = "'" <> concatMap escapeSingleQuote arg <> "'"
-  | otherwise = arg
-  where
-    needsQuote c = c == ' ' || c == '\t' || c == '\'' || c == '"'
-    escapeSingleQuote '\'' = "'\\''"
-    escapeSingleQuote c = [c]
diff --git a/src/Okf/Cli/CommandMetadata.hs b/src/Okf/Cli/CommandMetadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Okf/Cli/CommandMetadata.hs
@@ -0,0 +1,57 @@
+-- | Shared command descriptions and actionable examples for help and agents.
+module Okf.Cli.CommandMetadata (CommandGuide (..), commandGuides, commandDescription, commandExamples) where
+
+import Data.Aeson (ToJSON)
+import Data.List (find)
+import Data.Text (Text)
+import Data.Text qualified as Text
+import GHC.Generics (Generic)
+
+data CommandGuide = CommandGuide
+  { commandPath :: !Text,
+    description :: !Text,
+    examples :: ![Text],
+    writesFiles :: !Bool
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving anyclass (ToJSON)
+
+commandGuides :: [CommandGuide]
+commandGuides =
+  [ CommandGuide "bundles" "List discovered OKF bundles" ["okf bundles --json"] False,
+    CommandGuide "profiles" "List local profile descriptor paths; use `okf profile list` for all effective sources" ["okf profiles --json"] False,
+    CommandGuide "validate" "Validate an OKF bundle" ["okf validate BUNDLE --strict --profile PROFILE --profile-enforce"] False,
+    CommandGuide "index" "Preview or write generated index.md files" ["okf index BUNDLE", "okf index BUNDLE --write --okf-version 0.2"] True,
+    CommandGuide "log" "Preview and check log.md files" ["okf log BUNDLE", "okf log add BUNDLE --help"] True,
+    CommandGuide "graph" "Print a bundle graph" ["okf graph --help"] False,
+    CommandGuide "show" "Show one concept" ["okf show BUNDLE CONCEPT"] False,
+    CommandGuide "trust" "Report trust tiers, status, and staleness for every concept" ["okf trust --help"] False,
+    CommandGuide "sources" "List the provenance recorded by each concept" ["okf sources --help"] False,
+    CommandGuide "computations" "List the attested computations a bundle declares" ["okf computations --help"] False,
+    CommandGuide "concepts" "List the concepts a bundle holds, with optional filters" ["okf concepts BUNDLE --where status=accepted --json"] False,
+    CommandGuide "id" "Allocate and list document IDs" ["okf id list BUNDLE --profile PROFILE", "okf id next BUNDLE --profile PROFILE ADR"] False,
+    CommandGuide "alias" "List configured command aliases" ["okf alias --help"] False,
+    CommandGuide "config" "Show and manage okf configuration" ["okf config show", "okf config agent"] True,
+    CommandGuide "profile" "List and inspect profiles from registries and local descriptors" ["okf profile list --json", "okf profile show EXPORT", "okf profile init EXPORT --bundle BUNDLE --write"] True,
+    CommandGuide "kit" "Install and manage agent skills and subagents" ["okf kit list --json", "okf kit install NAME --project --tool-only"] True,
+    CommandGuide "agent" "Inspect OKF context or launch an interactive agent session" ["okf agent context --json", "okf agent assist"] True,
+    CommandGuide "assist" "Launch an interactive agent session with installed okf skills" ["okf assist --debug"] True,
+    CommandGuide "completions" "Generate a shell completion script (bash, zsh, fish)" ["okf completions --help"] False,
+    CommandGuide "help" "Show conceptual help topics" ["okf help agents"] False,
+    CommandGuide "agent assist" "Launch a contextual interactive agent session" ["okf agent assist", "okf agent assist --debug", "okf agent assist --provider codex --print-command"] True,
+    CommandGuide "agent context" "Inspect local OKF context without launching an agent" ["okf agent context --json"] False
+  ]
+
+commandDescription :: String -> String
+commandDescription name =
+  maybe name (\CommandGuide {description} -> Text.unpack description) (find (\CommandGuide {commandPath} -> commandPath == Text.pack name) commandGuides)
+
+-- | Parser help and live context use the same concrete command spellings.
+commandExamples :: [Text] -> String
+commandExamples paths =
+  Text.unpack
+    ( Text.unlines
+        ( "Examples:"
+            : ["  " <> example | CommandGuide {commandPath, examples} <- commandGuides, commandPath `elem` paths, example <- examples]
+        )
+    )
diff --git a/src/Okf/Cli/Config.hs b/src/Okf/Cli/Config.hs
--- a/src/Okf/Cli/Config.hs
+++ b/src/Okf/Cli/Config.hs
@@ -43,12 +43,14 @@
 where
 
 import Control.Exception (IOException, SomeException, catch)
+import Data.Generics.Labels ()
 import Data.Map.Strict (Map)
 import Data.Map.Strict qualified as Map
 import Data.Text qualified as Text
 import Dhall (FromDhall (..), auto, genericAutoWith)
 import Dhall qualified
 import Okf.Cli.Aliases (validateAliases)
+import Okf.Exception (catchSynchronous)
 import Okf.Prelude
 import Okf.Profile.Registry (defaultRegistryReference)
 import System.Directory (doesFileExist, getCurrentDirectory, getHomeDirectory)
@@ -424,7 +426,7 @@
     tryDecode :: IO a -> IO (Either Text a)
     tryDecode action =
       (Right <$> action)
-        `catch` \(exception :: SomeException) ->
+        `catchSynchronous` \(exception :: SomeException) ->
           pure (Left (Text.pack (show exception)))
 
 normalizeProfileConfig :: OkfConfig -> OkfConfig
diff --git a/src/Okf/Cli/Fzf.hs b/src/Okf/Cli/Fzf.hs
--- a/src/Okf/Cli/Fzf.hs
+++ b/src/Okf/Cli/Fzf.hs
@@ -39,7 +39,7 @@
 where
 
 import Control.Applicative ((<|>))
-import Control.Exception (SomeException, try)
+import Control.Exception (IOException, bracket, try)
 import Data.Map.Strict qualified as Map
 import Data.Maybe (fromMaybe, isJust)
 import Data.Text (Text)
@@ -52,9 +52,9 @@
 import System.Process
   ( CreateProcess (..),
     StdStream (CreatePipe, Inherit),
-    createProcess,
     proc,
     waitForProcess,
+    withCreateProcess,
   )
 
 -- | Everything needed to decide whether an interactive picker can run, probed
@@ -92,10 +92,8 @@
 
 checkTtyAvailable :: IO Bool
 checkTtyAvailable = do
-  opened <- try @SomeException (openFile "/dev/tty" ReadMode)
-  case opened of
-    Left _ -> pure False
-    Right handle -> hClose handle >> pure True
+  opened <- try @IOException (bracket (openFile "/dev/tty" ReadMode) hClose (const (pure ())))
+  pure (either (const False) (const True) opened)
 
 -- | An interactive picker is possible when fzf exists and there is a terminal
 -- to draw on.
@@ -207,8 +205,8 @@
 shellQuote :: Text -> Text
 shellQuote value = "'" <> Text.replace "'" "'\\''" value <> "'"
 
--- | Show a menu and return the chosen value. Never throws: every failure is a
--- 'FzfError'. An empty candidate list short-circuits without spawning fzf.
+-- | Show a menu and return the chosen value. Expected IO failures become
+-- 'FzfError'; asynchronous cancellation propagates after resource cleanup. An empty candidate list short-circuits without spawning fzf.
 runFzf :: FzfConfig -> FzfOpts -> [Candidate a] -> IO (FzfResult a)
 runFzf config opts candidates
   | null candidates = pure FzfNoMatch
@@ -224,8 +222,7 @@
                 std_err = Inherit,
                 delegate_ctlc = True
               }
-      outcome <- try @SomeException $ do
-        (maybeIn, maybeOut, _, processHandle) <- createProcess spec
+      outcome <- try @IOException $ withCreateProcess spec $ \maybeIn maybeOut _ processHandle ->
         case (maybeIn, maybeOut) of
           (Just inHandle, Just outHandle) -> do
             hSetEncoding inHandle utf8
diff --git a/src/Okf/Cli/Help.hs b/src/Okf/Cli/Help.hs
--- a/src/Okf/Cli/Help.hs
+++ b/src/Okf/Cli/Help.hs
@@ -17,6 +17,8 @@
     helpTopics,
     helpCommandParser,
     handleHelpCommand,
+    renderTopic,
+    resolveWidth,
   )
 where
 
@@ -26,11 +28,15 @@
 import Data.Text qualified as Text
 import Data.Text.IO qualified as Text.IO
 import Options.Applicative
+import System.Console.Terminal.Size qualified as Terminal
+import System.Exit (exitFailure)
+import System.IO (hIsTerminalDevice, stderr, stdout)
+import Text.Read (readMaybe)
 
 -- | A bare @help@ lists topics; @help <topic>@ shows one.
 data HelpCommand
   = ListTopics
-  | ShowTopic !Text
+  | ShowTopic !Text !(Maybe Int)
   deriving stock (Show, Eq)
 
 -- | A single help guide: short name, one-line description, embedded content.
@@ -118,24 +124,33 @@
 agentsTopicContent :: Text
 agentsTopicContent = $(embedStringFile "help/agents.md")
 
--- | Parse @help [TOPIC]@. With no argument, 'pure ListTopics' wins via '<|>'.
+-- | Bare help reliably lists topics. The width option is declared once.
 helpCommandParser :: Parser HelpCommand
 helpCommandParser =
-  showTopicParser <|> pure ListTopics
+  make <$> optional topicArgument <*> parserOptionGroup "Layout" widthOption
   where
-    showTopicParser =
-      ShowTopic . Text.pack
+    make Nothing _ = ListTopics
+    make (Just topic) width = ShowTopic topic width
+    topicArgument =
+      Text.pack
         <$> strArgument
           ( metavar "TOPIC"
-              <> help ("Help topic: " <> Text.unpack topicList)
+              <> help "Help topic; omit to list available topics"
+              <> completeWith (map (Text.unpack . topicName) helpTopics)
           )
-    topicList = Text.intercalate ", " (map topicName helpTopics)
+    widthOption =
+      optional $
+        option
+          (eitherReader positiveWidth)
+          (long "width" <> short 'w' <> metavar "COLUMNS" <> help "Wrap prose to a positive width; preserve examples and URLs")
+    positiveWidth raw = case readMaybe raw of
+      Just width | width > 0 -> Right width
+      _ -> Left "width must be a positive integer"
 
--- | Run the @help@ command: list the topic index or print one topic.
 handleHelpCommand :: HelpCommand -> IO ()
 handleHelpCommand = \case
   ListTopics -> listTopics
-  ShowTopic name -> showTopic name
+  ShowTopic name width -> showTopic name width
 
 listTopics :: IO ()
 listTopics = do
@@ -147,10 +162,54 @@
     -- 14, so the longest topic name ("computations") still gets two spaces.
     padRight n t = t <> Text.replicate (max 0 (n - Text.length t)) " "
 
-showTopic :: Text -> IO ()
-showTopic name =
+showTopic :: Text -> Maybe Int -> IO ()
+showTopic name requestedWidth =
   case find (\t -> topicName t == Text.toLower name) helpTopics of
-    Just t -> Text.IO.putStrLn (topicContent t)
+    Just topic -> do
+      width <- resolveWidth requestedWidth
+      -- Preserve the historical final writer newline when output is piped.
+      Text.IO.putStrLn (renderTopic width (topicContent topic))
     Nothing -> do
-      Text.IO.putStrLn ("Unknown topic: " <> name)
-      Text.IO.putStrLn ("Available: " <> Text.intercalate ", " (map topicName helpTopics))
+      Text.IO.hPutStrLn stderr ("Unknown topic: " <> name)
+      Text.IO.hPutStrLn stderr ("Use `okf help`; available: " <> Text.intercalate ", " (map topicName helpTopics))
+      exitFailure
+
+-- | ioctl only: never issue an escape sequence or wait for terminal input.
+resolveWidth :: Maybe Int -> IO (Maybe Int)
+resolveWidth (Just width) = pure (Just width)
+resolveWidth Nothing = do
+  terminal <- hIsTerminalDevice stdout
+  if not terminal
+    then pure Nothing
+    else do
+      window <- Terminal.size
+      pure $ case window of
+        Just Terminal.Window {Terminal.width = width} | width > 0 -> Just (min 140 width)
+        _ -> Nothing
+
+-- | Reflow only plain prose. Indentation, fenced blocks and unbreakable URLs
+-- are preserved. Explicit widths bypass the automatic readability cap.
+renderTopic :: Maybe Int -> Text -> Text
+renderTopic Nothing body = body
+renderTopic (Just width) body = Text.unlines (go Nothing (Text.lines body))
+  where
+    go _ [] = []
+    go fence (line : rest)
+      | Just marker <- fence = line : go (if marker `Text.isPrefixOf` Text.stripStart line then Nothing else fence) rest
+      | Just marker <- fenceMarker line = line : go (Just marker) rest
+      | protected line = line : go Nothing rest
+      | otherwise =
+          let (paragraph, remaining) = span (\lineValue -> not (protected lineValue) && fenceMarker lineValue == Nothing) rest
+           in wrapWords (Text.words (Text.unwords (line : paragraph))) <> go Nothing remaining
+    protected line = Text.null (Text.strip line) || "  " `Text.isPrefixOf` line || "\t" `Text.isPrefixOf` line
+    fenceMarker :: Text -> Maybe Text
+    fenceMarker line
+      | "```" `Text.isPrefixOf` Text.stripStart line = Just "```"
+      | "~~~" `Text.isPrefixOf` Text.stripStart line = Just "~~~"
+      | otherwise = Nothing
+    wrapWords [] = []
+    wrapWords (word : rest) = pack word rest
+    pack line [] = [line]
+    pack line (word : rest)
+      | Text.length line + 1 + Text.length word <= max 1 width = pack (line <> " " <> word) rest
+      | otherwise = line : pack word rest
diff --git a/src/Okf/Cli/Kit.hs b/src/Okf/Cli/Kit.hs
--- a/src/Okf/Cli/Kit.hs
+++ b/src/Okf/Cli/Kit.hs
@@ -9,16 +9,54 @@
 
 import Baikai.Kit.Command (KitCommand (..), OutputFormat (..))
 import Baikai.Kit.Command qualified as Engine
+import Baikai.Kit.Error (renderKitError)
+import Control.Exception (try)
+import Control.Lens ((&), (.~))
+import Data.Generics.Labels ()
+import Data.Text.IO qualified as Text.IO
 import Okf.Cli.Config (OkfConfig, defaultOkfConfig)
 import Okf.Cli.Kit.Config (kitConfig)
+import Okf.Cli.Kit.Selector
 import Options.Applicative
+import Options.Applicative.Internal (mapParserOptions)
+import Options.Applicative.Types (OptReader (..), Option (..))
+import System.Exit (ExitCode (..), exitWith)
+import System.IO (stderr)
 
 -- | Use the engine's parser so JSON output and visibility flags stay aligned
 -- with the installed engine. Configuration only supplies the tool name in help.
 kitCommandParser :: Parser KitCommand
-kitCommandParser = Engine.kitCommandParser (kitConfig defaultOkfConfig)
+kitCommandParser = decorateKitHelp (Engine.kitCommandParser (kitConfig defaultOkfConfig))
 
+-- The engine owns the parser and all its semantics. Group only the options in
+-- each existing child ParserInfo; grouping the outer parser cannot reach them.
+decorateKitHelp :: Parser a -> Parser a
+decorateKitHelp = mapParserOptions decorate
+  where
+    decorate :: Option a -> Option a
+    decorate kitOption@Option {optMain = CmdReader commandField children} =
+      kitOption {optMain = CmdReader commandField [(names, child {infoParser = parserOptionGroup "Kit scope, visibility and output" (infoParser child)}) | (names, child) <- children]}
+    decorate kitOption = kitOption
+
 -- | Run against the kit configuration derived from the loaded okf config.
 handleKitCommand :: OkfConfig -> KitCommand -> IO ()
-handleKitCommand config kitCommand =
-  Engine.runKit (kitConfig config) kitCommand
+handleKitCommand config kitCommand@(KitInstall Nothing _ _) = do
+  ready <- prepareKitChooser
+  case ready of
+    Left problem -> selectionError problem
+    Right picker -> do
+      result <- try @KitSelectionError (Engine.runKitCommand (kitConfig config & #chooseItem .~ Just (chooseKitItem picker)) kitCommand)
+      case result of
+        Left problem -> selectionError problem
+        Right (Left problem) -> Text.IO.hPutStrLn stderr (renderKitError problem) >> exitWith (ExitFailure 1)
+        Right (Right ()) -> pure ()
+handleKitCommand config kitCommand = Engine.runKit (kitConfig config) kitCommand
+
+selectionError :: KitSelectionError -> IO ()
+selectionError problem = do
+  let (code, message) = case problem of
+        KitSelectionUnavailable -> (2, "fzf and a controlling terminal are required; install fzf or use `okf kit install NAME`.")
+        KitSelectionEmpty -> (1, "The kit has no skills or subagents; use `okf kit list` or configure kit.repoUrl.")
+        KitSelectionFailed detail -> (1, detail)
+  Text.IO.hPutStrLn stderr ("okf kit install: " <> message)
+  exitWith (ExitFailure code)
diff --git a/src/Okf/Cli/Kit/Config.hs b/src/Okf/Cli/Kit/Config.hs
--- a/src/Okf/Cli/Kit/Config.hs
+++ b/src/Okf/Cli/Kit/Config.hs
@@ -7,6 +7,8 @@
 import Baikai.Interactive (InteractiveProvider (..))
 import Baikai.Kit.Config (KitConfig)
 import Baikai.Kit.Config qualified as Engine
+import Control.Lens ((&), (.~))
+import Data.Generics.Labels ()
 import Okf.Cli.Config (KitSettings (..), OkfConfig (..), OkfProvider (..))
 
 -- | Build the baikai-kit configuration from okf's loaded configuration. The
@@ -15,6 +17,7 @@
 kitConfig :: OkfConfig -> KitConfig
 kitConfig OkfConfig {kit = KitSettings {repoUrl = url, providers = providerList}} =
   Engine.kitConfig "okf" url (map toInteractive providerList)
+    & #projectRoot .~ Engine.projectRootByMarkers [".git", "okf-config.dhall", ".okf"]
 
 toInteractive :: OkfProvider -> InteractiveProvider
 toInteractive = \case
diff --git a/src/Okf/Cli/Kit/Selector.hs b/src/Okf/Cli/Kit/Selector.hs
new file mode 100644
--- /dev/null
+++ b/src/Okf/Cli/Kit/Selector.hs
@@ -0,0 +1,61 @@
+-- | Application-owned kit presentation; Baikai retains the installer lifecycle.
+module Okf.Cli.Kit.Selector
+  ( KitSelectionError (..),
+    kitCandidates,
+    prepareKitChooser,
+    chooseKitItem,
+  )
+where
+
+import Baikai.Kit.Manifest (KitItem (..), KitManifest (..), itemKind, itemName)
+import Control.Exception (Exception, throwIO)
+import Control.Lens ((^.))
+import Data.Char (isControl)
+import Data.Generics.Labels ()
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Okf.Cli.Fzf
+
+data KitSelectionError
+  = KitSelectionUnavailable
+  | KitSelectionEmpty
+  | KitSelectionFailed !Text
+  deriving stock (Show, Eq)
+
+instance Exception KitSelectionError
+
+-- | The index maps back to original identity, never to sanitized display text.
+kitCandidates :: KitManifest -> [Candidate Text]
+kitCandidates manifest =
+  [ Candidate (label item description) (itemName item)
+  | (item, description) <-
+      [(KitSkillItem entry, entry ^. #description) | entry <- manifest ^. #skills]
+        <> [(KitAgentItem entry, entry ^. #description) | entry <- manifest ^. #agents]
+  ]
+  where
+    safe = Text.filter (not . isControl)
+    label item description = safe (itemName item) <> " [" <> itemKind item <> "] " <> safe description
+
+-- | Probe before the engine can clone or refresh anything.
+prepareKitChooser :: IO (Either KitSelectionError FzfConfig)
+prepareKitChooser = do
+  config <- detectFzfConfig
+  pure $
+    if fzfAvailable config && ttyAvailable config
+      then Right config
+      else Left KitSelectionUnavailable
+
+-- | Only explicit cancellation crosses the engine seam as Nothing. Errors keep
+-- their type across its Maybe callback and are caught at the CLI boundary.
+chooseKitItem :: FzfConfig -> KitManifest -> IO (Maybe Text)
+chooseKitItem config manifest
+  | null candidates = throwIO KitSelectionEmpty
+  | otherwise = do
+      selected <- runFzf config (withPrompt "Install kit item> " <> withHeader "Name [kind] description; Esc cancels" <> withNoSort) candidates
+      case selected of
+        FzfSelected name -> pure (Just name)
+        FzfCancelled -> pure Nothing
+        FzfNoMatch -> throwIO (KitSelectionFailed "No kit item matched; use `okf kit list` or `okf kit install NAME`.")
+        FzfError message -> throwIO (KitSelectionFailed message)
+  where
+    candidates = kitCandidates manifest
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,15 +1,23 @@
 module Main (main) where
 
+import Baikai.Interactive (CodexApprovalPolicy (..), CodexSandboxMode (..), InteractiveSafety (..), interactiveLaunchRequest)
 import Baikai.Kit.Config (KitScope (..))
 import Baikai.Kit.Install (InstallOptions (..), OverwritePolicy (..), defaultInstallOptions)
+import Baikai.Kit.Manifest qualified as Manifest
 import Baikai.Kit.Visibility (KitVisibility (..))
-import Control.Exception (bracket, try)
+import Baikai.Provider.Claude.Interactive (claudeInteractiveCommand, defaultClaudeInteractiveConfig)
+import Baikai.Provider.OpenAI.Interactive (codexInteractiveCommand, defaultCodexInteractiveConfig)
+import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar, threadDelay)
+import Control.Exception (AsyncException (UserInterrupt), bracket, throwTo, try)
+import Control.Lens ((&), (.~), (^.))
 import Control.Monad (unless)
 import Data.Aeson (Value (..), toJSON)
 import Data.Aeson qualified as Aeson
 import Data.Aeson.KeyMap qualified as KeyMap
 import Data.Bifunctor (first)
+import Data.Char (isControl)
 import Data.Foldable (toList, traverse_)
+import Data.Generics.Labels ()
 import Data.List qualified as List
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.Map.Strict qualified as Map
@@ -20,31 +28,55 @@
 import Okf.Bundle (Concept, bundleInventoryOfConcepts, conceptAttester, conceptExecutor, conceptFromDocument, conceptIdOf, conceptParameters, conceptRuntime, conceptType, walkBundle, walkBundleInventory)
 import Okf.Cli
 import Okf.Cli.Agent.Config (AgentCommandName (..), AgentConfigSource (..), AgentField (..), AgentOverrides (..), ResolvedAgent (..), ResolvedField (..), agentSourceLabel, noAgentOverrides, parseOkfEffort, parseOkfProvider, renderAgentResolution, resolveAgent)
+import Okf.Cli.Agent.Context qualified as Context
 import Okf.Cli.Aliases (AliasCommand (..), expandAlias, isAliasCandidate, renderAliases, validateAliases)
-import Okf.Cli.Assist (AssistOptions (..), buildAgentCommand, buildSessionAgentCommand)
+import Okf.Cli.Assist (AssistInspection (..), AssistOptions (..), buildAgentCommand, buildSessionAgentCommand)
 import Okf.Cli.BundleDiscovery (BundleDiscovery (..), bundleSearchRootsEnvVar, discoverAvailableBundles)
+import Okf.Cli.CommandMetadata qualified as Metadata
 import Okf.Cli.Config (AgentFieldSettings (..), AgentSettings (..), ConfigSource (..), KitSettings (..), OkfConfig (..), OkfEffort (..), OkfProvider (..), ProfileSettings (..), agentSharedDefaults, defaultOkfConfig, exampleConfigText, findConfigSource, loadAgentScopes, loadAliasesForExpansion, loadOkfConfig, okfConfigEnvVar, projectConfigPath, renderConfig)
-import Okf.Cli.Fzf (Candidate (..), FzfConfig (..), FzfOpts (..), optsToArgs, parseSelectionIndex, renderCandidateLines, shellQuote, withAnsi, withHeight, withNoSort, withPrompt)
+import Okf.Cli.Fzf (Candidate (..), FzfConfig (..), FzfOpts (..), optsToArgs, parseSelectionIndex, renderCandidateLines, runFzf, shellQuote, withAnsi, withHeight, withNoSort, withPrompt)
 import Okf.Cli.Fzf.Selector (ConceptOrder (..), conceptCandidates, conceptPreviewCommand, orderConcepts, parseBundleSearchRoots, profileCandidates, profilePreviewCommand)
-import Okf.Cli.Help (HelpTopic (..), helpTopics)
+import Okf.Cli.Help (HelpTopic (..), helpTopics, renderTopic)
 import Okf.Cli.Kit (KitCommand (..), OutputFormat (..))
+import Okf.Cli.Kit.Selector (kitCandidates)
 import Okf.Cli.ProfileDiscovery (ProfileDiscovery (..), discoverAvailableProfiles, parseProfileSearchRoots, profileSearchRootsEnvVar)
 import Okf.ConceptId (ConceptId, parseConceptId, renderConceptId)
 import Okf.Document (Attester (..), Executor (..), Parameter (..), parseDocument)
 import Okf.Index (OkfVersion (..), VersionDeclaration (..), parseOkfVersion, readBundleVersion)
-import Okf.Profile (Cardinality (..), CompiledProfile, FieldCondition (..), FieldFormat (..), FieldPath (..), FieldPathSegment (..), FieldRule (..), FrontmatterRules (..), HandleReferenceRule (..), NestedFieldRule (..), NestedRules (..), PathReferenceRule (..), ProfileSpec (..), ProfileViolation (..), TypeRule (..), compileProfile, loadProfileFile, validateProfile, validateProfileVersion)
+import Okf.Profile (Cardinality (..), CompiledProfile, FieldCondition (..), FieldFormat (..), FieldPath (..), FieldPathSegment (..), FieldRule (..), FrontmatterRules (..), HandleReferenceRule (..), NestedFieldRule (..), NestedRules (..), PathReferenceRule (..), ProfileSpec (..), ProfileViolation (..), TypeRule (..), compileProfile, compiledProfileSpec, loadProfileFile, validateProfile, validateProfileVersion)
 import Okf.Profile.Registry (ProfileSource (..), ProfileSourceLoadError (..), RegistryEntry (..), RegistryLoadError (..), RegistryRef (..), SourcedProfile (..), defaultRegistryReference)
 import Okf.Query (ConceptFilter (..), ConceptPredicate (..), FieldSelector (..), SortDirection (..), SortKey (..), WhereCondition (..), filterConcepts, filterConceptsWhere, parseSortKey, parseWhereCondition, sortConcepts)
 import Okf.Validation (ValidationProfile (..), validateBundle)
 import Options.Applicative
 import System.Directory (Permissions (..), canonicalizePath, createDirectoryIfMissing, createFileLink, doesDirectoryExist, doesFileExist, getCurrentDirectory, getPermissions, getTemporaryDirectory, listDirectory, makeAbsolute, removeDirectoryRecursive, setModificationTime, setPermissions, withCurrentDirectory)
-import System.Environment (lookupEnv, setEnv, unsetEnv, withArgs)
+import System.Environment (getArgs, getExecutablePath, lookupEnv, setEnv, unsetEnv, withArgs)
 import System.Exit (ExitCode (..), exitFailure)
 import System.FilePath (normalise, takeDirectory, (</>))
 import System.IO.Temp (createTempDirectory)
+import System.Timeout (timeout)
 
 main :: IO ()
 main = do
+  arguments <- getArgs
+  if "--select-1" `elem` arguments then fzfFixture arguments else runTests
+
+fzfFixture :: [String] -> IO ()
+fzfFixture arguments = do
+  _ <- Text.IO.getContents
+  case promptPath arguments of
+    Just path -> writeFile path "ready"
+    Nothing -> pure ()
+  threadDelay 30000000
+  where
+    promptPath ("--prompt" : path : _) = Just path
+    promptPath (_ : rest) = promptPath rest
+    promptPath [] = Nothing
+
+runTests :: IO ()
+runTests = do
+  nestedClosureCli <- testNestedClosureCli
+  productDiscoveryCli <- testProductDiscoveryCli
+  fzfCancellation <- testFzfCancellation
   bundleDiscoveryListing <- testBundleDiscoveryListing
   profileDiscoveryListing <- testProfileDiscoveryListing
   profilePickerExitCodes <- testProfilePickerExitCodes
@@ -95,7 +127,11 @@
   conceptMenuOrdering <- testConceptMenuOrdering
   nonAsciiDiagnostics <- testNonAsciiValuesSurviveDiagnostics
   let results =
-        [ parseBundlesMatches ["bundles"] (BundlesOptions False),
+        [ nestedClosureCli,
+          productDiscoveryCli,
+          fzfCancellation,
+          testHelpWrapping,
+          parseBundlesMatches ["bundles"] (BundlesOptions False),
           parseBundlesMatches ["bundles", "--json"] (BundlesOptions True),
           parseProfilesMatches ["profiles"] (ProfilesOptions False),
           parseProfilesMatches ["profiles", "--json"] (ProfilesOptions True),
@@ -343,6 +379,12 @@
           parseSucceeds ["kit", "uninstall", "demo-skill"],
           parseSucceeds ["kit", "uninstall", "demo-skill", "--project"],
           parseSucceeds ["kit", "status"],
+          parseSucceeds ["agent", "assist"],
+          parseSucceeds ["assist"],
+          parseSucceeds ["agent", "context", "--json"],
+          parseFails ["agent", "assist", "--debug", "--print-command"],
+          testContextRendering,
+          List.sort builtinCommands == List.sort [Metadata.commandPath guide | guide <- Metadata.commandGuides, not (Text.any (== ' ') (Metadata.commandPath guide))],
           parseSucceeds ["assist", "Summarize this bundle"],
           parseSucceeds ["assist", "--print-command", "Summarize this bundle"],
           parseSucceeds ["assist", "--model", "claude-opus-4-5", "Summarize this bundle"],
@@ -710,6 +752,8 @@
           assistModelOverride,
           assistCodexCommandBuilder,
           assistKitVisibility,
+          testKitCandidates,
+          parseCommandMatches ["kit", "install"] (Kit (KitInstall Nothing UserScope defaultInstallOptions)),
           parseCommandMatches ["kit"] (Kit (KitList HumanOutput)),
           parseCommandMatches ["kit", "list", "--json"] (Kit (KitList JsonOutput)),
           parseCommandMatches ["kit", "status", "--json"] (Kit (KitStatus JsonOutput)),
@@ -720,6 +764,7 @@
           parseCommandMatches ["kit", "uninstall", "author", "--project"] (Kit (KitUninstall "author" ProjectScope)),
           testAssistEffortReachesEachVendor,
           testAssistUnconfiguredRendersNoFlags,
+          testInteractivePolicyRefusals,
           testAgentFlagBeatsEverything,
           testAgentEnvBeatsBothScopes,
           testAgentCommandKeyBeatsDefaultKeyInScope,
@@ -843,6 +888,7 @@
   [ render (ValueNotInVocabulary cid prefecture ["東京都", "京都府"] japanese),
     render (CardinalityMismatch cid prefecture List japanese),
     render (ValueFormatMismatch cid prefecture Uri japanese),
+    render (TextOperandMismatch cid prefecture japanese),
     render (MalformedDocumentReference cid prefecture japanese),
     render (MalformedPathReference cid prefecture japanese),
     render (NestedElementNotRecord cid nestedElement (toJSON ["東京" :: Text.Text]))
@@ -1089,18 +1135,18 @@
   let rendered = renderProfileSourceResolution sampleProfileSourceResolutions FreshnessNotChecked
    in all
         (`Text.isInfixOf` rendered)
-        [ "okf-profiles v0.19.0 (pinned)",
+        [ "okf-profiles v0.25.0 (pinned)",
           "[built-in default]",
           "[env: OKF_PROFILE_REGISTRIES]",
           "failed (import-failure)",
-          "Pinned catalogue: okf-profiles v0.19.0",
+          "Pinned catalogue: okf-profiles v0.25.0",
           "1. --registry flag (repeatable)",
           "lookup fails closed."
         ]
 
 testProfileSourcesJsonShape :: Bool
 testProfileSourcesJsonShape =
-  profileSourcesJson sampleProfileSourceResolutions (FreshnessOutdated "v0.19.0" "v0.20.0")
+  profileSourcesJson sampleProfileSourceResolutions (FreshnessOutdated "v0.25.0" "v0.26.0")
     == Aeson.object
       [ "sources"
           Aeson..= [ Aeson.object
@@ -1130,13 +1176,13 @@
                              ]
                        ]
                    ],
-        "pinnedVersion" Aeson..= Just ("v0.19.0" :: Text.Text),
+        "pinnedVersion" Aeson..= Just ("v0.25.0" :: Text.Text),
         "freshness"
           Aeson..= Aeson.object
             [ "status" Aeson..= ("outdated" :: Text.Text),
-              "pinnedVersion" Aeson..= ("v0.19.0" :: Text.Text),
-              "latestVersion" Aeson..= ("v0.20.0" :: Text.Text),
-              "refreshCommand" Aeson..= ("scripts/refresh-default-registry.sh v0.20.0" :: Text.Text)
+              "pinnedVersion" Aeson..= ("v0.25.0" :: Text.Text),
+              "latestVersion" Aeson..= ("v0.26.0" :: Text.Text),
+              "refreshCommand" Aeson..= ("scripts/refresh-default-registry.sh v0.26.0" :: Text.Text)
             ],
         "precedence" Aeson..= profileSourcePrecedenceFixture
       ]
@@ -1161,7 +1207,7 @@
     && parseReleaseVersionTag "v0.9.3" == Just (ReleaseVersion 0 9 3)
     && parseReleaseVersionTag "0.10.0" == Nothing
     && parseReleaseVersionTag "v0.10" == Nothing
-    && pinnedRegistryTag defaultRegistryReference == Just "v0.19.0"
+    && pinnedRegistryTag defaultRegistryReference == Just "v0.25.0"
     && latestReleaseTag
       "aaa\trefs/tags/v0.9.3\nbbb\trefs/tags/not-a-version\nccc\trefs/tags/v0.10.0\n"
       == Just "v0.10.0"
@@ -1206,7 +1252,11 @@
       okfVersion = "0.1",
       frontmatter =
         FrontmatterRules
-          { required = [undocumentedField "type", undocumentedField "title"],
+          { membershipConstraints = [],
+            uniqueValuesConstraints = [],
+            presenceConstraints = [],
+            timestampOrderConstraints = [],
+            required = [undocumentedField "type", undocumentedField "title"],
             recommended = [],
             optional = []
           },
@@ -1219,7 +1269,7 @@
             { type_ = "PostgreSQL Table",
               description = Nothing,
               guidance = Nothing,
-              frontmatter = FrontmatterRules {required = [], recommended = [], optional = []},
+              frontmatter = FrontmatterRules {membershipConstraints = [], uniqueValuesConstraints = [], presenceConstraints = [], timestampOrderConstraints = [], required = [], recommended = [], optional = []},
               pathPattern = Just "schemas/*/tables/*",
               resourceScheme = Just "postgresql",
               requireSchemaSection = True,
@@ -1240,7 +1290,11 @@
       okfVersion = "0.1",
       frontmatter =
         FrontmatterRules
-          { required =
+          { membershipConstraints = [],
+            uniqueValuesConstraints = [],
+            presenceConstraints = [],
+            timestampOrderConstraints = [],
+            required =
               [ FieldRule
                   { field = "type",
                     description = Just "The OKF concept type; must be a type rule below.",
@@ -1284,7 +1338,11 @@
               guidance = Just "Explain the trade-offs.\n\nRecord follow-up work.",
               frontmatter =
                 FrontmatterRules
-                  { required = [FieldRule "owner" (Just "Person responsible for the decision.") [] Scalar (Just (DocumentHandle "USR")) Nothing Nothing Nothing Nothing Nothing Nothing],
+                  { membershipConstraints = [],
+                    uniqueValuesConstraints = [],
+                    presenceConstraints = [],
+                    timestampOrderConstraints = [],
+                    required = [FieldRule "owner" (Just "Person responsible for the decision.") [] Scalar (Just (DocumentHandle "USR")) Nothing Nothing Nothing Nothing Nothing Nothing],
                     recommended = [FieldRule "reviewer" Nothing ["Ari", "Bo"] List Nothing Nothing Nothing (Just (handleReferenceRule "ADR" ["mori"] False)) Nothing Nothing Nothing],
                     optional = [FieldRule "supersedes" Nothing [] Scalar Nothing Nothing Nothing (Just (handleReferenceRule "ADR" [] False)) Nothing Nothing Nothing]
                   },
@@ -1317,7 +1375,11 @@
       okfVersion = "0.1",
       frontmatter =
         FrontmatterRules
-          { required =
+          { membershipConstraints = [],
+            uniqueValuesConstraints = [],
+            presenceConstraints = [],
+            timestampOrderConstraints = [],
+            required =
               [ FieldRule
                   "reviews"
                   Nothing
@@ -1326,7 +1388,10 @@
                   Nothing
                   ( Just
                       NestedRules
-                        { required =
+                        { allowUnknownFields = True,
+                          presenceConstraints = [],
+                          timestampOrderConstraints = [],
+                          required =
                             [ (nestedFieldRule "outcome" Nothing ["approved", "rejected"] Any Nothing Nothing (Just (FieldCondition "kind" ["model"])))
                                 { reference =
                                     Just
@@ -1356,7 +1421,10 @@
                   Nothing
                   ( Just
                       NestedRules
-                        { required = [nestedFieldRule "resource" (Just "The skill a runner follows.") [] Scalar Nothing (Just (PathReferenceRule [] False)) Nothing],
+                        { allowUnknownFields = True,
+                          presenceConstraints = [],
+                          timestampOrderConstraints = [],
+                          required = [nestedFieldRule "resource" (Just "The skill a runner follows.") [] Scalar Nothing (Just (PathReferenceRule [] False)) Nothing],
                           recommended = [],
                           optional = [nestedFieldRule "receipt" Nothing [] List Nothing Nothing Nothing]
                         }
@@ -1398,6 +1466,7 @@
     "    uniqueBy: outcome",
     "    objectFields: (none)",
     "    elementFields:",
+    "      allowUnknownFields: true",
     "      required:",
     "        - outcome: (none)",
     "          allowedValues: approved, rejected",
@@ -1422,6 +1491,8 @@
     "          reference: (none)",
     "          path: (none)",
     "          when: (none)",
+    "      Presence constraints: none",
+    "      Timestamp order constraints: none",
     "  - executor: Run instructions and the receipt fields a run must return.",
     "    allowedValues: (any)",
     "    cardinality: any",
@@ -1431,6 +1502,7 @@
     "    when: (none)",
     "    uniqueBy: (none)",
     "    objectFields:",
+    "      allowUnknownFields: true",
     "      required:",
     "        - resource: The skill a runner follows.",
     "          allowedValues: (any)",
@@ -1448,9 +1520,15 @@
     "          reference: (none)",
     "          path: (none)",
     "          when: (none)",
+    "      Presence constraints: none",
+    "      Timestamp order constraints: none",
     "    elementFields: (none)",
     "frontmatter.recommended: (none)",
-    "frontmatter.optional: (none)"
+    "frontmatter.optional: (none)",
+    "Presence constraints: none",
+    "Timestamp order constraints: none",
+    "Membership constraints (text operands): none",
+    "Unique values constraints (text operands): none"
   ]
 
 -- | Every optional field prints, as @(none)@ when absent, so the shape does not
@@ -1503,6 +1581,10 @@
     "    uniqueBy: (none)",
     "    objectFields: (none)",
     "    elementFields: (none)",
+    "Presence constraints: none",
+    "Timestamp order constraints: none",
+    "Membership constraints (text operands): none",
+    "Unique values constraints (text operands): none",
     "",
     "type: Decision Record",
     "  description: One accepted decision, never edited after acceptance.",
@@ -1543,6 +1625,10 @@
     "      uniqueBy: (none)",
     "      objectFields: (none)",
     "      elementFields: (none)",
+    "  Presence constraints: none",
+    "  Timestamp order constraints: none",
+    "  Membership constraints (text operands): none",
+    "  Unique values constraints (text operands): none",
     "  pathPattern: decisions/*",
     "  resourceScheme: (none)",
     "  requireSchemaSection: false",
@@ -1587,6 +1673,10 @@
     "    elementFields: (none)",
     "frontmatter.recommended: (none)",
     "frontmatter.optional: (none)",
+    "Presence constraints: none",
+    "Timestamp order constraints: none",
+    "Membership constraints (text operands): none",
+    "Unique values constraints (text operands): none",
     "",
     "type: PostgreSQL Table",
     "  description: (none)",
@@ -1594,6 +1684,10 @@
     "  frontmatter.required: (none)",
     "  frontmatter.recommended: (none)",
     "  frontmatter.optional: (none)",
+    "  Presence constraints: none",
+    "  Timestamp order constraints: none",
+    "  Membership constraints (text operands): none",
+    "  Unique values constraints (text operands): none",
     "  pathPattern: schemas/*/tables/*",
     "  resourceScheme: postgresql",
     "  requireSchemaSection: true",
@@ -3336,14 +3430,14 @@
       rendered <- buildSessionAgentCommand config codex prompt
       claudeRendered <- buildSessionAgentCommand config claude prompt
       pure $
-        withoutSkills == buildAgentCommand codex [] prompt
-          && claudeRendered == buildAgentCommand claude [] prompt
+        either (const False) (\(_, args) -> "# OKF project context" `List.isInfixOf` last args) withoutSkills
+          && either (const False) (\(_, args) -> "--append-system-prompt" `elem` args && any (List.isInfixOf "# OKF project context") args) claudeRendered
           && case rendered of
             Left _ -> False
             Right (executable, args) ->
               executable == "codex"
                 && take 1 args == ["-c"]
-                && last args == "do work"
+                && "do work" `List.isSuffixOf` last args
                 && case args of
                   _ : override : _ ->
                     "skills.config=[" `List.isPrefixOf` override
@@ -3357,12 +3451,12 @@
 assistPrompt :: Text.Text -> AssistOptions
 assistPrompt promptText =
   AssistOptions
-    { prompt = promptText,
+    { prompt = Just promptText,
       providerOverride = Nothing,
       modelOverride = Nothing,
       effortOverride = Nothing,
       systemPromptOverride = Nothing,
-      printCommand = False
+      inspection = LaunchSession
     }
 
 assistTestAgent :: OkfProvider -> Maybe OkfEffort -> ResolvedAgent
@@ -3680,3 +3774,153 @@
       invalidDescriptor <- doesFileExist (invalidBundle </> "profile.dhall")
       structuralOk <- success "post-write validation" (invalidResult == Left (ExitFailure 1) && invalidLog && invalidDescriptor)
       pure (and [previewOk, greenOk, dateOk, dateSyntaxOk, directoryOk, danglingOk, danglingBundleOk, fileTargetOk, versionOk, existingOk, invalidOk, mismatchOk, indexFailureOk, logFailureOk, structuralOk])
+
+-- A refusal is an error-shaped value, so it cannot be mistaken for an argv
+-- that a consumer may spawn. Keep this compatibility boundary on upgrades.
+testInteractivePolicyRefusals :: Bool
+testInteractivePolicyRefusals =
+  refused (claudeInteractiveCommand defaultClaudeInteractiveConfig (request (CodexSandbox CodexReadOnly CodexApprovalOnRequest)))
+    && refused (codexInteractiveCommand defaultCodexInteractiveConfig (request (ClaudeAllowedTools ["Read"])))
+    && all
+      (\approval -> refused (codexInteractiveCommand defaultCodexInteractiveConfig (request (CodexSandbox CodexReadOnly approval))))
+      [CodexApprovalUntrusted, CodexApprovalOnFailure]
+  where
+    request policy = interactiveLaunchRequest "inspect" & #safety .~ policy
+    refused = either (const True) (const False)
+
+testContextRendering :: Bool
+testContextRendering =
+  all (`Text.isInfixOf` markdown) ["/project/ü", "fixture-profile", "Author guidance", "API: Type guidance", "okf bundles --json"]
+    && Aeson.decode (Context.renderContextJson fixture) == Just (toJSON fixture)
+  where
+    fixture =
+      Context.AgentContext
+        1
+        "/project/ü"
+        "/project"
+        ["/project/bundle"]
+        [Context.ProfileContext "/project/profile.dhall" "fixture-profile" Nothing (Just "Author guidance") [("API", "Type guidance")]]
+        Metadata.commandGuides
+        []
+        []
+    markdown = Context.renderContextMarkdown fixture
+
+testKitCandidates :: Bool
+testKitCandidates =
+  map candidateValue candidates == ["same\nname", "samename"]
+    && case map candidateDisplay candidates of
+      [firstLabel, secondLabel] -> firstLabel == secondLabel && not (Text.any isControl firstLabel)
+      _ -> False
+  where
+    entry name = Manifest.SkillEntry name "description\t" Nothing "skills/demo" ["SKILL.md"] Nothing
+    candidates = kitCandidates (Manifest.KitManifest 2 [entry "same\nname", entry "samename"] [])
+
+testHelpWrapping :: Bool
+testHelpWrapping =
+  renderTopic Nothing body == body
+    && all (`Text.isInfixOf` rendered) [example, url, fenced]
+    && all ((<= 60) . Text.length) (takeWhile (not . Text.null) (Text.lines rendered))
+    && Text.words prose == Text.words (Text.unlines (takeWhile (not . Text.null) (Text.lines rendered)))
+  where
+    prose = Text.unwords (replicate 20 "ordinary prose words")
+    example = "  okf agent assist --system-prompt 'an example preserved verbatim even when it exceeds the width'"
+    url = "https://example.invalid/a/long/unbreakable/url/that/must/remain/a/single/token"
+    fenced = "```bash\n" <> Text.replicate 90 "x" <> "\n```"
+    body = Text.intercalate "\n\n" [prose, example, url, fenced] <> "\n"
+    rendered = renderTopic (Just 60) body
+
+testFzfCancellation :: IO Bool
+testFzfCancellation = withIsolatedConfigEnv "okf-fzf-cancellation" $ do
+  executable <- getExecutablePath
+  root <- getCurrentDirectory
+  let marker = root </> "ready"
+      config = FzfConfig executable True False False True
+      awaitReady 0 = pure False
+      awaitReady attempts = do
+        exists <- doesFileExist marker
+        if exists then pure True else threadDelay 1000 >> awaitReady (attempts - 1 :: Int)
+  finished <- newEmptyMVar
+  worker <- forkIO $ do
+    outcome <- try @AsyncException (runFzf config (withPrompt (Text.pack marker)) [Candidate "fixture" ("fixture" :: Text.Text)])
+    putMVar finished outcome
+  ready <- awaitReady 2000
+  throwTo worker UserInterrupt
+  outcome <- timeout 2000000 (takeMVar finished)
+  pure $
+    ready && case outcome of
+      Just (Left UserInterrupt) -> True
+      _ -> False
+
+-- Exercise enforced/advisory exit behavior through the public command runner.
+testProductDiscoveryCli :: IO Bool
+testProductDiscoveryCli = withRepositoryPath2 "product discovery CLI" "okf-core/test/fixtures/profiles/product-discovery.dhall" "okf-core/test/fixtures" $ \descriptor fixtures -> do
+  let run name enforce =
+        try @ExitCode $
+          runCommand $
+            Validate
+              ValidateOptions
+                { bundlePath = Just (fixtures </> name),
+                  strictMode = True,
+                  profilePath = Just descriptor,
+                  pickProfile = False,
+                  profileEnforce = enforce,
+                  logEnforce = True
+                }
+  valid <- run "profile-product-discovery-valid" True
+  invalid <- run "profile-product-discovery-invalid" True
+  advisory <- run "profile-product-discovery-invalid" False
+  loaded <- loadProfileFile descriptor
+  let surfaces = case loaded >>= first (Text.pack . show) . compileProfile of
+        Left _ -> False
+        Right compiled ->
+          let spec = compiledProfileSpec compiled
+              detail = Text.unlines (renderProfileDetail "productDiscovery" spec)
+              encoded = Text.pack (show (Aeson.toJSON spec))
+           in all (`Text.isInfixOf` detail) ["Presence constraints", "Timestamp order", "Membership constraints", "Unique values", "finite-decimal"]
+                && all (`Text.isInfixOf` encoded) ["presenceConstraints", "timestampOrderConstraints", "membershipConstraints", "uniqueValuesConstraints"]
+  pure (valid == Right () && invalid == Left (ExitFailure 1) && advisory == Right () && surfaces)
+
+testNestedClosureCli :: IO Bool
+testNestedClosureCli = withRepositoryPath2 "nested closure CLI" "okf-core/test/fixtures/profiles/nested-closure.dhall" "okf-core/test/fixtures" $ \descriptor fixtures -> do
+  let run bundle strict enforce =
+        try @ExitCode $
+          runCommand $
+            Validate
+              ValidateOptions
+                { bundlePath = Just (fixtures </> bundle),
+                  strictMode = strict,
+                  profilePath = Just descriptor,
+                  pickProfile = False,
+                  profileEnforce = enforce,
+                  logEnforce = True
+                }
+  results <-
+    sequence
+      [ run "profile-nested-closure-valid" True True,
+        run "profile-nested-closure-invalid" True True,
+        run "profile-nested-closure-invalid" True False,
+        run "profile-nested-closure-invalid" False True,
+        run "profile-nested-closure-invalid" False False
+      ]
+  loaded <- loadProfileFile descriptor
+  let surfaces = case loaded >>= first (Text.pack . show) . compileProfile of
+        Left _ -> False
+        Right compiled ->
+          let spec = compiledProfileSpec compiled
+              detail = Text.unlines (renderProfileDetail "nestedClosure" spec)
+              declared = [rule | rule <- spec ^. #frontmatter . #optional] <> [rule | ty <- spec ^. #types, rule <- ty ^. #frontmatter . #optional]
+              policies = [KeyMap.lookup "allowUnknownFields" fields | rule <- declared, Just nested <- [rule ^. #elementFields], Aeson.Object fields <- [toJSON nested]]
+              objectPolicies = [KeyMap.lookup "allowUnknownFields" fields | rule <- declared, Just nested <- [rule ^. #objectFields], Aeson.Object fields <- [toJSON nested]]
+           in "allowUnknownFields: false" `Text.isInfixOf` detail
+                && "allowUnknownFields: true" `Text.isInfixOf` detail
+                && policies == [Just (Bool False), Just (Bool True)]
+                && objectPolicies == [Just (Bool False)]
+                && case parseConceptId "note" of
+                  Left _ -> False
+                  Right cid ->
+                    renderProfileViolation
+                      compiled
+                      []
+                      (NestedFieldNotInProfile cid (FieldPath (FieldName "rows" :| [ArrayIndex 3, FieldName "名前"])))
+                      == "note: nested field not declared by profile: rows[3].名前"
+  pure (results == [Right (), Left (ExitFailure 1), Right (), Left (ExitFailure 1), Right ()] && surfaces)
