diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14d5c40..deb634a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to the Toolpath workspace are documented here.
+## `meta.kind` — new path-kind field; conversation paths tagged `agent-coding-session` — unreleased
+
+New optional `meta.kind` field on `Path` (`toolpath::v1::PathMeta::kind`, plus
+the `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION` constant) — a hint to renderers and
+generic parsers that a path follows a recognizable shape. The only defined
+value is `"agent-coding-session"`: each step is a `conversation.append` change carrying that
+turn's `role`, `text`, and so on, and `meta.source` names the producing
+harness. Absent means generic — existing documents parse and validate
+unchanged, and `kind` is omitted when unset.
+
+Every conversation → `Path` derivation now sets `meta.kind = "agent-coding-session"` (the
+shared `toolpath_convo::derive_path` and each conversation provider crate's
+own). The JSONL form carries `kind` through `PathOpen.meta` and `PathMeta`
+patch lines. Documented in the RFC ("Document Kind") and the JSON Schema
+(`$defs/pathMeta`).
+
+Touches `toolpath`, `toolpath-convo`, `toolpath-claude`, `toolpath-gemini`,
+`toolpath-codex`, `toolpath-opencode`, and `toolpath-pi`; versions to be
+bumped at release.
+
## `path resume` — one-shot resume into a coding agent — 2026-05-09
`path-cli` 0.9.0. New subcommand `path resume ` that fetches a
diff --git a/CLAUDE.md b/CLAUDE.md
index 1556973..34686ab 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -221,6 +221,7 @@ Build the site after changes: `cd site && pnpm run build` (should produce 7 page
- `toolpath-gemini` treats main file + sibling sub-agent UUID dir as one conversation. Sub-agent files are folded into `DelegatedWork` with populated `turns` (unlike `toolpath-claude`, whose sub-agent turns live in separate session files and stay empty). See `docs/agents/formats/gemini.md` for the full format reference.
- Provider-specific extras convention: `Turn.extra` and `WatcherEvent::Progress.data` use provider-namespaced keys (e.g. `extra["claude"]`, `extra["gemini"]`). `toolpath-claude` populates `Turn.extra["claude"]` from `ConversationEntry.extra`; `toolpath-gemini` populates `Turn.extra["gemini"]` with the full `tokens` struct, per-thought metadata, and tool-call status. This lets trait-only consumers access provider metadata without importing provider types.
- Shared derivation: `toolpath-convo` provides a provider-agnostic `ConversationView → Path` mapping via `toolpath_convo::derive_path`. New conversation providers should build on it rather than re-implementing the mapping.
+- Path kinds: `toolpath::v1::PathMeta` has an optional `kind` field — a hint to renderers/parsers about the path's shape. Currently the only value is `"agent-coding-session"` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`): a path where each step is a `conversation.append` change carrying that turn's content. Every conversation→`Path` derivation sets it — the shared `derive_path` and each provider crate's own (`toolpath-claude`/`-gemini`/`-codex`/`-opencode`; `-pi` inherits it via the shared mapping). It rides through the JSONL form too (`PathOpen.meta` / `PathMeta` patch lines). Documented in `RFC.md` ("Document Kind") and the JSON Schema's `$defs/pathMeta`.
- Pi provider: `toolpath-pi` reads Pi session JSONL from `~/.pi/agent/sessions/`. Sessions use a tree (id/parentId) in a single file, and may link to a parent file via `parentSession` in the header. The tree is preserved as a DAG in the derived `Path`.
- Codex provider: `toolpath-codex` reads Codex CLI rollout files from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. Sessions are date-bucketed (not project-keyed). File-change fidelity is excellent — Codex's `patch_apply_end` events carry either the unified diff (for updates) or the full file content (for adds), so the derived `Path` gets a real `raw` perspective on every file artifact. See `docs/agents/formats/codex.md` for the full format reference.
- opencode provider: `toolpath-opencode` reads a SQLite database at `~/.local/share/opencode/opencode.db` (opened read-only). Each session's messages and 12 typed part variants (text, reasoning, tool, step-start/-finish, snapshot, patch, file, agent, subtask, retry, compaction) land as one step per message with tool invocations attached. File diffs come from a sibling bare git repo at `snapshot//[]/` via `git2` tree↔tree diffs — opencode respects the user's `.gitignore`, so changes under gitignored paths fall back to tool-input-derived structural changes with no `raw` perspective. Project id is the SHA of the repo's first root commit. See `docs/agents/formats/opencode.md` for the full format reference.
diff --git a/RFC.md b/RFC.md
index a3d062c..90c3dde 100644
--- a/RFC.md
+++ b/RFC.md
@@ -169,7 +169,7 @@ A **path** collects steps and provides root context:
| -------- | --------------------------------------------------- |
| `path` | Identity, base context, and head reference |
| `steps` | Array of step objects |
-| `meta` | Path-level metadata (title, actors, signatures) |
+| `meta` | Path-level metadata (title, kind, actors, signatures) |
The `path.base` anchors the entire tree to a specific state (repo + ref +
commit). Steps within inherit this context.
@@ -276,11 +276,30 @@ paths.
| Field | Description |
| ------------ | -------------------------------------------------- |
+| `kind` | Path kind — see [Document Kind](#document-kind) (paths only) |
| `intent` | Human-readable description of purpose |
| `refs` | Links to issues, docs, reasoning |
| `actors` | Actor definitions with identities and keys |
| `signatures` | Cryptographic signatures for verification |
+#### Document Kind
+
+`meta.kind` on a **path** classifies it — a hint that the path follows a
+recognizable shape worth special-casing. It is always optional; an absent or
+unrecognized `kind` should be treated as a generic path. The only value defined
+so far is **`agent-coding-session`** (future revisions may register more).
+
+A **`agent-coding-session`** path is an AI coding conversation. Each conversational-turn step
+carries one [`ArtifactChange`](#change-perspectives) whose `structural.type` is
+`"conversation.append"` — find it by that `type`, not by artifact key. That
+change's `structural` object always has `role` (`"user"` / `"assistant"` /
+`"system"` / producer-specific) and, when the turn has prose, `text`; it may
+also carry `thinking`, `tool_uses`, token counts, `stop_reason`, environment
+fields, and a producer-namespaced bag of anything else. `meta.source` names the
+producing harness (`claude-code`, `gemini-cli`, `codex`, `opencode`, `pi`);
+structure beyond the `conversation.append` change — synthetic steps, file-write
+artifacts, tool records — is producer-specific.
+
#### Actor Definitions
`meta.actors` maps actor strings to full definitions:
@@ -565,7 +584,7 @@ The path provides:
- **base**: Where this tree branches from (repo + ref + commit)
- **head**: Current tip of the active path
- **steps**: All steps including dead ends (step-001a has no descendants)
-- **meta**: Path-level metadata including actors and signatures
+- **meta**: Path-level metadata including `kind` (see [Document Kind](#document-kind)), actors, and signatures
### Base Context
diff --git a/crates/toolpath-claude/src/derive.rs b/crates/toolpath-claude/src/derive.rs
index ec4907b..ad84b33 100644
--- a/crates/toolpath-claude/src/derive.rs
+++ b/crates/toolpath-claude/src/derive.rs
@@ -12,8 +12,8 @@ use std::collections::HashMap;
use std::path::Path as FsPath;
use std::process::Command;
use toolpath::v1::{
- ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
- StepIdentity, StructuralChange,
+ ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
+ PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::file_write_diff;
@@ -611,6 +611,7 @@ pub fn derive_path(conversation: &Conversation, config: &DeriveConfig) -> Path {
steps,
meta: Some(PathMeta {
title: Some(format!("Claude session: {}", session_short)),
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("claude-code".to_string()),
actors: if actors.is_empty() {
None
@@ -768,6 +769,21 @@ mod tests {
assert!(path.steps[1].step.actor.starts_with("agent:"));
}
+ #[test]
+ fn test_derive_path_meta_kind_is_convo() {
+ let convo = make_conversation(vec![make_entry(
+ "uuid-1111-aaaa",
+ MessageRole::User,
+ "Hello",
+ "2024-01-01T00:00:00Z",
+ )]);
+ let path = derive_path(&convo, &DeriveConfig::default());
+ assert_eq!(
+ path.meta.as_ref().unwrap().kind.as_deref(),
+ Some(PATH_KIND_AGENT_CODING_SESSION)
+ );
+ }
+
#[test]
fn test_derive_path_step_parents() {
let entries = vec![
diff --git a/crates/toolpath-codex/src/derive.rs b/crates/toolpath-codex/src/derive.rs
index bfca648..bc082b2 100644
--- a/crates/toolpath-codex/src/derive.rs
+++ b/crates/toolpath-codex/src/derive.rs
@@ -16,8 +16,8 @@ use crate::types::{PatchChange, Session};
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use toolpath::v1::{
- ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
- StepIdentity, StructuralChange,
+ ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
+ PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::{ConversationView, Role, Turn};
@@ -172,6 +172,7 @@ fn derive_path_from_view(
steps,
meta: Some(PathMeta {
title: Some(format!("Codex session: {}", session_short)),
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("codex".to_string()),
actors: if actors.is_empty() {
None
@@ -584,6 +585,17 @@ mod tests {
assert!(actors.contains_key("agent:gpt-5.4"));
}
+ #[test]
+ fn derive_path_meta_kind_is_convo() {
+ let (_t, mgr, id) = fixture_session(&minimal_body());
+ let session = mgr.read_session(&id).unwrap();
+ let path = derive_path(&session, &DeriveConfig::default());
+ assert_eq!(
+ path.meta.as_ref().unwrap().kind.as_deref(),
+ Some(PATH_KIND_AGENT_CODING_SESSION)
+ );
+ }
+
#[test]
fn derive_path_preserves_conversation_artifact() {
let (_t, mgr, id) = fixture_session(&minimal_body());
diff --git a/crates/toolpath-convo/src/derive.rs b/crates/toolpath-convo/src/derive.rs
index 2b2886c..619e606 100644
--- a/crates/toolpath-convo/src/derive.rs
+++ b/crates/toolpath-convo/src/derive.rs
@@ -3,13 +3,15 @@
//! Provider-agnostic mapping used by the Pi, Claude, and future conversation
//! providers. Takes a [`ConversationView`] and emits a [`Path`] document with
//! one step per turn and a `conversation.append` structural change carrying
-//! the turn's text, thinking, tool uses, and token usage.
+//! the turn's text, thinking, tool uses, and token usage. The emitted path is
+//! tagged with `meta.kind = "agent-coding-session"` (`toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`)
+//! so renderers and parsers know it follows the conversation shape.
use std::collections::HashMap;
use toolpath::v1::{
- ActorDefinition, ArtifactChange, Base, Path, PathIdentity, PathMeta, Step, StepIdentity,
- StructuralChange,
+ ActorDefinition, ArtifactChange, Base, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta,
+ Step, StepIdentity, StructuralChange,
};
use crate::{ConversationView, Role, ToolCategory, ToolInvocation, Turn};
@@ -243,6 +245,7 @@ pub fn derive_path(view: &ConversationView, config: &DeriveConfig) -> Path {
let mut meta = PathMeta {
title: Some(title),
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: view.provider_id.clone(),
..Default::default()
};
@@ -519,6 +522,19 @@ mod tests {
assert_eq!(path.path.head, "");
}
+ #[test]
+ fn test_meta_kind_is_convo() {
+ let view = view_with(vec![base_turn("t1", Role::User)]);
+ let path = derive_path(&view, &DeriveConfig::default());
+ assert_eq!(
+ path.meta.as_ref().unwrap().kind.as_deref(),
+ Some(PATH_KIND_AGENT_CODING_SESSION)
+ );
+ // ...and survives a JSON round-trip.
+ let json = serde_json::to_string(&path).unwrap();
+ assert!(json.contains(r#""kind":"agent-coding-session""#));
+ }
+
#[test]
fn test_single_user_turn() {
let mut turn = base_turn("t1", Role::User);
diff --git a/crates/toolpath-gemini/src/derive.rs b/crates/toolpath-gemini/src/derive.rs
index f0bc090..c4eb807 100644
--- a/crates/toolpath-gemini/src/derive.rs
+++ b/crates/toolpath-gemini/src/derive.rs
@@ -15,8 +15,8 @@ use crate::types::{ChatFile, Conversation, GeminiMessage, GeminiRole, ToolCall};
use serde_json::json;
use std::collections::HashMap;
use toolpath::v1::{
- ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
- StepIdentity, StructuralChange,
+ ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
+ PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::ToolCategory;
@@ -126,6 +126,7 @@ pub fn derive_path(conversation: &Conversation, config: &DeriveConfig) -> Path {
session_short
}
)),
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("gemini-cli".to_string()),
actors: if actors.is_empty() {
None
@@ -518,6 +519,16 @@ mod tests {
assert_eq!(path.path.head, path.steps.last().unwrap().step.id);
}
+ #[test]
+ fn test_derive_path_meta_kind_is_convo() {
+ let convo = main_only_convo();
+ let path = derive_path(&convo, &DeriveConfig::default());
+ assert_eq!(
+ path.meta.as_ref().unwrap().kind.as_deref(),
+ Some(PATH_KIND_AGENT_CODING_SESSION)
+ );
+ }
+
#[test]
fn test_derive_path_parents_chain() {
let convo = main_only_convo();
diff --git a/crates/toolpath-opencode/src/derive.rs b/crates/toolpath-opencode/src/derive.rs
index cfb74c4..409aa0d 100644
--- a/crates/toolpath-opencode/src/derive.rs
+++ b/crates/toolpath-opencode/src/derive.rs
@@ -18,8 +18,8 @@ use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::path::{Path as StdPath, PathBuf};
use toolpath::v1::{
- ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
- StepIdentity, StructuralChange,
+ ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
+ PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::{ConversationView, Role, Turn};
@@ -165,6 +165,7 @@ fn derive_path_from_view(
steps,
meta: Some(PathMeta {
title: Some(format!("opencode session: {}", session.title)),
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("opencode".to_string()),
actors: if actors.is_empty() {
None
@@ -691,6 +692,24 @@ mod tests {
assert!(actors.contains_key("agent:claude-sonnet-4-6"));
}
+ #[test]
+ fn derive_meta_kind_is_convo() {
+ let (_t, mgr, resolver) = fixture(BASIC_SQL);
+ let s = mgr.read_session("ses_abc123").unwrap();
+ let p = derive_path_with_resolver(
+ &s,
+ &DeriveConfig {
+ no_snapshot_diffs: true,
+ ..Default::default()
+ },
+ &resolver,
+ );
+ assert_eq!(
+ p.meta.as_ref().unwrap().kind.as_deref(),
+ Some(PATH_KIND_AGENT_CODING_SESSION)
+ );
+ }
+
#[test]
fn derive_fallback_file_artifact_from_tool() {
let (_t, mgr, resolver) = fixture(BASIC_SQL);
diff --git a/crates/toolpath-pi/src/derive.rs b/crates/toolpath-pi/src/derive.rs
index d60a8be..7b04deb 100644
--- a/crates/toolpath-pi/src/derive.rs
+++ b/crates/toolpath-pi/src/derive.rs
@@ -106,6 +106,11 @@ mod tests {
"got: {}",
path.path.id
);
+ // The shared derivation tags conversation paths with `meta.kind`.
+ assert_eq!(
+ path.meta.as_ref().unwrap().kind.as_deref(),
+ Some(toolpath::v1::PATH_KIND_AGENT_CODING_SESSION)
+ );
}
#[test]
diff --git a/crates/toolpath/schema/toolpath.schema.json b/crates/toolpath/schema/toolpath.schema.json
index aab7962..22db4f2 100644
--- a/crates/toolpath/schema/toolpath.schema.json
+++ b/crates/toolpath/schema/toolpath.schema.json
@@ -336,6 +336,11 @@
"type": "string",
"description": "Human-readable title"
},
+ "kind": {
+ "type": "string",
+ "description": "The kind of this path — a hint to renderers and parsers that it follows a particular shape and may carry kind-specific conventions. Currently the only defined value is \"convo\": a path derived from an agent conversation, where each step is a `conversation.append` change carrying that turn's content. Absent means generic — no kind-specific structure is implied.",
+ "examples": ["agent-coding-session"]
+ },
"source": {
"type": "string",
"description": "Source reference (e.g., PR URL)"
diff --git a/crates/toolpath/src/jsonl.rs b/crates/toolpath/src/jsonl.rs
index 69f5898..d9da2e0 100644
--- a/crates/toolpath/src/jsonl.rs
+++ b/crates/toolpath/src/jsonl.rs
@@ -101,6 +101,8 @@ pub struct PathOpenMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
+ pub kind: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option,
@@ -148,6 +150,8 @@ pub struct PathMetaPatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
+ pub kind: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option,
@@ -347,6 +351,7 @@ impl Path {
let mut meta = PathMeta::default();
if let Some(m) = po.meta {
meta.title = m.title;
+ meta.kind = m.kind;
meta.source = m.source;
meta.intent = m.intent;
meta.refs = m.refs;
@@ -504,6 +509,9 @@ fn apply_meta_patch(path_meta: &mut PathMeta, patch: PathMetaPatch) {
if let Some(v) = patch.title {
path_meta.title = Some(v);
}
+ if let Some(v) = patch.kind {
+ path_meta.kind = Some(v);
+ }
if let Some(v) = patch.source {
path_meta.source = Some(v);
}
@@ -546,6 +554,7 @@ fn resolve_head(explicit: Option, steps: &[Step]) -> Result bool {
m.title.is_none()
+ && m.kind.is_none()
&& m.source.is_none()
&& m.intent.is_none()
&& m.refs.is_empty()
@@ -670,12 +679,14 @@ fn step_meta_is_empty(m: &StepMeta) -> bool {
fn path_meta_for_open(m: &PathMeta) -> Option {
let open = PathOpenMeta {
title: m.title.clone(),
+ kind: m.kind.clone(),
source: m.source.clone(),
intent: m.intent.clone(),
refs: m.refs.clone(),
extra: m.extra.clone(),
};
if open.title.is_none()
+ && open.kind.is_none()
&& open.source.is_none()
&& open.intent.is_none()
&& open.refs.is_empty()
@@ -1222,6 +1233,38 @@ mod tests {
assert_eq!(canonical_json(&p), canonical_json(&back));
}
+ #[test]
+ fn roundtrip_kind_in_path_meta() {
+ let p = Path {
+ path: PathIdentity {
+ id: "p".into(),
+ base: None,
+ head: "s1".into(),
+ graph_ref: None,
+ },
+ steps: vec![make_step("s1", None)],
+ meta: Some(PathMeta {
+ kind: Some(crate::v1::PATH_KIND_AGENT_CODING_SESSION.to_string()),
+ ..Default::default()
+ }),
+ };
+ let jsonl = p.to_jsonl_string().unwrap();
+ assert!(jsonl.contains(r#""kind":"agent-coding-session""#));
+ let back = Path::from_jsonl_str(&jsonl).unwrap();
+ assert_eq!(canonical_json(&p), canonical_json(&back));
+ }
+
+ #[test]
+ fn path_meta_line_can_set_kind() {
+ let patch = PathMetaPatch {
+ kind: Some("agent-coding-session".into()),
+ ..Default::default()
+ };
+ let mut meta = PathMeta::default();
+ apply_meta_patch(&mut meta, patch);
+ assert_eq!(meta.kind.as_deref(), Some("agent-coding-session"));
+ }
+
#[test]
fn change_artifact_roundtrip_preserved() {
// Sanity check that we don't mangle ArtifactChange fields through
diff --git a/crates/toolpath/src/lib.rs b/crates/toolpath/src/lib.rs
index 19fe93b..8974fa3 100644
--- a/crates/toolpath/src/lib.rs
+++ b/crates/toolpath/src/lib.rs
@@ -50,6 +50,7 @@ pub mod v1 {
//! Optional annotations for richer context:
//!
//! - [`StepMeta`], [`PathMeta`], [`GraphMeta`] — metadata containers
+ //! - [`PATH_KIND_AGENT_CODING_SESSION`] — value for [`PathMeta::kind`] on conversation-derived paths
//! - [`ActorDefinition`] — full actor details (name, provider, keys)
//! - [`Identity`] — external identity reference
//! - [`Key`] — cryptographic key reference
@@ -146,7 +147,7 @@ pub mod v1 {
pub use crate::types::{
ActorDefinition, ArtifactChange, Base, Graph, GraphIdentity, GraphMeta, Identity, Key,
- Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature, Step, StepIdentity,
- StepMeta, StructuralChange, VcsSource,
+ PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature,
+ Step, StepIdentity, StepMeta, StructuralChange, VcsSource,
};
}
diff --git a/crates/toolpath/src/types.rs b/crates/toolpath/src/types.rs
index a6551bd..a95b7c2 100644
--- a/crates/toolpath/src/types.rs
+++ b/crates/toolpath/src/types.rs
@@ -140,12 +140,18 @@ pub struct Base {
pub branch: Option,
}
+/// [`PathMeta::kind`] value for a path derived from an AI coding conversation.
+/// See the Toolpath RFC's "Document Kind" section.
+pub const PATH_KIND_AGENT_CODING_SESSION: &str = "agent-coding-session";
+
/// Path metadata
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PathMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
+ pub kind: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option,
@@ -810,6 +816,24 @@ mod tests {
assert!(json.contains("issues/1"));
}
+ #[test]
+ fn test_path_meta_kind_serde() {
+ let meta = PathMeta {
+ kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
+ ..Default::default()
+ };
+ let json = serde_json::to_string(&meta).unwrap();
+ assert!(json.contains(r#""kind":"agent-coding-session""#));
+ let parsed: PathMeta = serde_json::from_str(&json).unwrap();
+ assert_eq!(parsed.kind.as_deref(), Some("agent-coding-session"));
+ }
+
+ #[test]
+ fn test_path_meta_kind_omitted_when_none() {
+ let json = serde_json::to_string(&PathMeta::default()).unwrap();
+ assert!(!json.contains("kind"));
+ }
+
#[test]
fn test_identity_serialization() {
let id = super::Identity {