deepvista-cli 0.1.18__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. deepvista_cli-0.2.0/.release-please-manifest.json +3 -0
  2. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/CHANGELOG.md +8 -0
  3. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/PKG-INFO +1 -1
  4. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/agent_catalog.py +22 -9
  5. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/session.py +24 -0
  6. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/config.py +57 -2
  7. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  8. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/README.md +19 -0
  9. deepvista_cli-0.2.0/plugins/claude-code/commands/deepvista.md +129 -0
  10. deepvista_cli-0.2.0/plugins/claude-code/skills/daily-planning/SKILL.md +161 -0
  11. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/pyproject.toml +1 -1
  12. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/session.md +31 -0
  13. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/uv.lock +1 -1
  14. deepvista_cli-0.1.18/.release-please-manifest.json +0 -3
  15. deepvista_cli-0.1.18/tests/test_agent_catalog.py +0 -225
  16. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.claude-plugin/marketplace.json +0 -0
  17. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  18. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  19. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.github/workflows/release-please.yml +0 -0
  20. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.gitignore +0 -0
  21. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/.pre-commit-config.yaml +0 -0
  22. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/CLAUDE.md +0 -0
  23. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/CONTRIBUTING.md +0 -0
  24. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/LICENSE +0 -0
  25. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/README.md +0 -0
  26. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/__init__.py +0 -0
  27. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/auth/__init__.py +0 -0
  28. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/auth/callback_server.py +0 -0
  29. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/auth/login.py +0 -0
  30. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/auth/tokens.py +0 -0
  31. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/client/__init__.py +0 -0
  32. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/client/http.py +0 -0
  33. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/client/origin.py +0 -0
  34. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/__init__.py +0 -0
  35. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/agents.py +0 -0
  36. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/auth.py +0 -0
  37. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/card.py +0 -0
  38. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/chat.py +0 -0
  39. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/config.py +0 -0
  40. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/lint.py +0 -0
  41. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/memory.py +0 -0
  42. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/notes.py +0 -0
  43. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/skill.py +0 -0
  44. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/upgrade.py +0 -0
  45. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/commands/vistabase.py +0 -0
  46. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/main.py +0 -0
  47. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/output/__init__.py +0 -0
  48. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/output/formatter.py +0 -0
  49. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/resources/__init__.py +0 -0
  50. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/resources/workflow_host_runtime.md +0 -0
  51. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/session_note.py +0 -0
  52. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/skill_catalog.py +0 -0
  53. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/tui/__init__.py +0 -0
  54. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/tui/app.py +0 -0
  55. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/deepvista_cli/workflow_doc.py +0 -0
  56. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/docs/assets/deepvista-banner.png +0 -0
  57. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/install.sh +0 -0
  58. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/README.md +0 -0
  59. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/agents/.gitignore +0 -0
  60. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/commands/refresh-skills.md +0 -0
  61. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/hooks/hooks.json +0 -0
  62. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/scripts/deepvista-session-end.sh +0 -0
  63. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/scripts/deepvista-session-start.sh +0 -0
  64. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/scripts/deepvista-session-turn.sh +0 -0
  65. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/scripts/deepvista-skill-url.py +0 -0
  66. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/scripts/deepvista-sync.sh +0 -0
  67. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/skills/.gitignore +0 -0
  68. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/plugins/claude-code/skills/install-deepvista-cli/SKILL.md +0 -0
  69. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/release-please-config.json +0 -0
  70. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/scripts/check_plugin_version.py +0 -0
  71. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/SKILL.md +0 -0
  72. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/chat.md +0 -0
  73. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/lint.md +0 -0
  74. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/memory.md +0 -0
  75. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/notes.md +0 -0
  76. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/openclaw.md +0 -0
  77. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/shared.md +0 -0
  78. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/skill-analyze-notes.md +0 -0
  79. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/skill-create-from-note.md +0 -0
  80. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/skill-import-files.md +0 -0
  81. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/skill-research-to-skill.md +0 -0
  82. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/skill.md +0 -0
  83. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/vistabase-card.md +0 -0
  84. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/deepvista/reference/vistabase.md +0 -0
  85. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/skills/dv-workflow/SKILL.md +0 -0
  86. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/tests/__init__.py +0 -0
  87. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/tests/test_agent_id_tagging.py +0 -0
  88. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/tests/test_session_note_format.py +0 -0
  89. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/tests/test_skill_catalog.py +0 -0
  90. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/tests/test_skill_commands.py +0 -0
  91. {deepvista_cli-0.1.18 → deepvista_cli-0.2.0}/uninstall.sh +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.0"
3
+ }
@@ -37,6 +37,14 @@ users what's new between the version they have installed and the latest release.
37
37
  adopts a pre-existing server-side row instead of failing when the local
38
38
  file is missing.
39
39
 
40
+ ## [0.2.0](https://github.com/DeepVista-AI/deepvista-cli/compare/v0.1.18...v0.2.0) (2026-05-28)
41
+
42
+
43
+ ### Features
44
+
45
+ * **DV-853:** productionize the deepvista subagents ([#147](https://github.com/DeepVista-AI/deepvista-cli/issues/147)) ([45e9ba1](https://github.com/DeepVista-AI/deepvista-cli/commit/45e9ba17de08243fe84be3de6826c72116360351))
46
+ * **session:** skip session init for configured CWD patterns (DV-862) ([#148](https://github.com/DeepVista-AI/deepvista-cli/issues/148)) ([df74381](https://github.com/DeepVista-AI/deepvista-cli/commit/df743810ae6dec7c4a520c43d82f1eeae421762b))
47
+
40
48
  ## [0.1.18](https://github.com/DeepVista-AI/deepvista-cli/compare/v0.1.17...v0.1.18) (2026-05-28)
41
49
 
42
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepvista-cli
3
- Version: 0.1.18
3
+ Version: 0.2.0
4
4
  Summary: CLI for DeepVista — chat, notes, skills, and memory from your terminal.
5
5
  Project-URL: Homepage, https://deepvista.ai
6
6
  Project-URL: Repository, https://github.com/DeepVista-AI/deepvista-cli
@@ -105,7 +105,12 @@ class AgentRoleMeta:
105
105
  agent_type: str = "" # e.g. "claude-code"
106
106
  updated_at: str = ""
107
107
  count: int = 1 # how many managed agents share this role
108
- system_prompt: str = "" # custom prompt from the agent's config.soul (DV-836); baked as the body
108
+ # Plain-text system prompt baked verbatim as the subagent body. Sourced
109
+ # from the managed agent's ``config.system_prompt`` (preferred) or its
110
+ # older ``config.soul`` (fallback). Persona-skill references are NOT
111
+ # resolved at export time — the prompt should instruct the agent to load
112
+ # the persona itself (e.g. "follow persona context card xxxx").
113
+ system_prompt: str = ""
109
114
 
110
115
  @property
111
116
  def slug(self) -> str:
@@ -207,14 +212,17 @@ def build_agent_markdown(meta: AgentRoleMeta) -> str:
207
212
  frontmatter.append(f"x-deepvista-updated-at: {meta.updated_at}")
208
213
  frontmatter.append("---")
209
214
 
210
- # A custom system prompt (config.soul) is authoritative bake it in as the body
211
- # verbatim. Frontmatter (routing, tools, model, preloaded skill) stays templated.
215
+ # A custom system prompt (config.system_prompt, falling back to config.soul)
216
+ # is authoritative bake it in as the body verbatim. Frontmatter (routing,
217
+ # tools, model, preloaded skill) stays templated. The prompt itself is the
218
+ # right place to reference a persona context card by id ("…follow persona
219
+ # context card xxxx"); the agent loads it at runtime via the deepvista skill.
212
220
  if meta.system_prompt.strip():
213
221
  prompt_body = (
214
- "<!-- Generated by `deepvista agents export` from the configured system prompt\n"
215
- f" (config.soul) of your “{meta.agent_name}” managed agent · role: {meta.role}\n"
216
- f" · agent id: {meta.agent_id or 'n/a'}. Edit it in DeepVista — local changes\n"
217
- " are overwritten on the next sync. -->\n\n"
222
+ "<!-- Generated by `deepvista agents export` from the configured system\n"
223
+ f" prompt of your “{meta.agent_name}” managed agent · role: {meta.role}\n"
224
+ f" · agent id: {meta.agent_id or 'n/a'}. Edit it in DeepVista — local\n"
225
+ " changes are overwritten on the next sync. -->\n\n"
218
226
  f"{meta.system_prompt.strip()}\n"
219
227
  )
220
228
  return "\n".join(frontmatter) + "\n\n" + prompt_body
@@ -290,7 +298,12 @@ def metas_from_agents(agents: list[dict[str, Any]]) -> list[AgentRoleMeta]:
290
298
  continue
291
299
  updated = str(agent.get("updated_at") or "")
292
300
  config = agent.get("config") or {}
293
- soul = str(config.get("soul") or "").strip()
301
+ # DV-853: ``config.system_prompt`` is the public field for the
302
+ # subagent body. Falls back to the legacy ``config.soul`` so existing
303
+ # managed agents keep working unchanged. Plain text; persona-card
304
+ # references are left to the prompt's wording (the agent loads them
305
+ # at runtime via the preloaded `deepvista` skill).
306
+ prompt_body = str(config.get("system_prompt") or config.get("soul") or "").strip()
294
307
  candidate = AgentRoleMeta(
295
308
  role=raw_role,
296
309
  agent_name=str(agent.get("name") or ""),
@@ -298,7 +311,7 @@ def metas_from_agents(agents: list[dict[str, Any]]) -> list[AgentRoleMeta]:
298
311
  agent_type=str(agent.get("agent_type") or ""),
299
312
  updated_at=updated,
300
313
  count=1,
301
- system_prompt=soul,
314
+ system_prompt=prompt_body,
302
315
  )
303
316
  existing = by_role.get(slug)
304
317
  if existing is None:
@@ -23,6 +23,7 @@ import click
23
23
  from deepvista_cli import session_note as sn
24
24
  from deepvista_cli.client.http import DeepVistaClient
25
25
  from deepvista_cli.client.origin import detect_agent_tool
26
+ from deepvista_cli.config import should_skip_session_cwd
26
27
  from deepvista_cli.output.formatter import format_output, output_error
27
28
 
28
29
  SESSION_CARD_TYPE = "session"
@@ -116,6 +117,29 @@ def session_init(
116
117
  """
117
118
  from deepvista_cli.commands.agents import load_agent_id_for_active_agent
118
119
 
120
+ if should_skip_session_cwd(cwd):
121
+ # DV-862: CWD matches ``session_skip_cwd_patterns`` in
122
+ # ``~/.config/deepvista/config.json``. Drops sub-agent sessions
123
+ # whose CWD isn't a real working directory — e.g. claude-mem's
124
+ # observer sub-claude runs with
125
+ # ``cwd=~/.claude-mem/observer-sessions`` and quotes file paths
126
+ # from the *primary* session, polluting the vistabase. Downstream
127
+ # ``tick`` / ``finalize`` self-no-op (no cached card → exit 3) so
128
+ # guarding ``init`` alone covers the whole lifecycle.
129
+ format_output(
130
+ {
131
+ "skipped": True,
132
+ "reason": "cwd matches session_skip_cwd_patterns",
133
+ "cwd": cwd,
134
+ "session_id": session_id,
135
+ },
136
+ ctx.obj.output_format,
137
+ title="Session init (skipped)",
138
+ entity_type=SESSION_ENTITY_TYPE,
139
+ base_url=ctx.obj.auth_url,
140
+ )
141
+ return
142
+
119
143
  if agent is None:
120
144
  detected_agent, detected_version = detect_agent_tool()
121
145
  agent = detected_agent
@@ -8,6 +8,7 @@ Resolution order for each setting:
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import fnmatch
11
12
  import json
12
13
  import os
13
14
  from dataclasses import dataclass
@@ -36,6 +37,21 @@ def credentials_path(profile: str = "default") -> Path:
36
37
 
37
38
  PROFILES_PATH = CONFIG_DIR / "config.json"
38
39
 
40
+ # Reserved top-level key in ``config.json`` holding the list of CWD globs
41
+ # that ``deepvista session init`` should skip. Stored as a flat list (not a
42
+ # dict) so it's trivially distinguishable from profile entries and easy for
43
+ # the user to hand-edit. See DV-862.
44
+ SESSION_SKIP_CWD_KEY = "session_skip_cwd_patterns"
45
+
46
+ # Defaults used when ``config.json`` is missing the key. Skips claude-mem's
47
+ # observer sub-claude (cwd=~/.claude-mem/observer-sessions) so its quoted
48
+ # file paths from the *primary* session don't get mined into bogus File
49
+ # cards (root cause for DV-861).
50
+ DEFAULT_SESSION_SKIP_CWD_PATTERNS: tuple[str, ...] = (
51
+ "*/.claude-mem/observer-sessions",
52
+ "*/.claude-mem/observer-sessions/*",
53
+ )
54
+
39
55
 
40
56
  # ---------------------------------------------------------------------------
41
57
  # Exit codes (following GWS pattern)
@@ -84,8 +100,13 @@ def set_profile(name: str, settings: dict) -> None:
84
100
 
85
101
 
86
102
  def list_profiles() -> dict:
87
- """List all profiles."""
88
- return _load_profiles()
103
+ """List all profiles.
104
+
105
+ Filters out reserved non-profile keys (e.g.
106
+ ``session_skip_cwd_patterns``) by keeping only dict-valued entries —
107
+ profiles are always dicts, reserved settings are lists/scalars.
108
+ """
109
+ return {k: v for k, v in _load_profiles().items() if isinstance(v, dict)}
89
110
 
90
111
 
91
112
  def delete_profile(name: str) -> bool:
@@ -98,6 +119,40 @@ def delete_profile(name: str) -> bool:
98
119
  return False
99
120
 
100
121
 
122
+ # ---------------------------------------------------------------------------
123
+ # Session CWD skip patterns (DV-862)
124
+ # ---------------------------------------------------------------------------
125
+
126
+
127
+ def get_session_skip_cwd_patterns() -> list[str]:
128
+ """Return the configured CWD-skip patterns for ``session init``.
129
+
130
+ Reads a top-level ``session_skip_cwd_patterns`` list from
131
+ ``~/.config/deepvista/config.json``. There is no matching setter — edit
132
+ the file by hand. Falls back to ``DEFAULT_SESSION_SKIP_CWD_PATTERNS``
133
+ when the key is absent. An explicit empty list disables skipping.
134
+ """
135
+ raw = _load_profiles().get(SESSION_SKIP_CWD_KEY)
136
+ if isinstance(raw, list):
137
+ return [str(p) for p in raw]
138
+ return list(DEFAULT_SESSION_SKIP_CWD_PATTERNS)
139
+
140
+
141
+ def should_skip_session_cwd(cwd: str | None) -> bool:
142
+ """Return True iff ``cwd`` matches any configured skip pattern.
143
+
144
+ Matching is fnmatch-style (Unix shell globs; ``*`` matches across path
145
+ separators). The hook payload's ``cwd`` is already absolute on every
146
+ supported agent, so no resolution is performed here.
147
+ """
148
+ if not cwd:
149
+ return False
150
+ for pattern in get_session_skip_cwd_patterns():
151
+ if fnmatch.fnmatchcase(cwd, pattern):
152
+ return True
153
+ return False
154
+
155
+
101
156
  # ---------------------------------------------------------------------------
102
157
  # Runtime config — resolved once per invocation
103
158
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepvista",
3
- "version": "0.1.18",
3
+ "version": "0.2.0",
4
4
  "description": "Remote-managed skill catalog from DeepVista. Auto-syncs thin stubs into the plugin's skills dir on SessionStart; full bodies are lazy-loaded at invocation time.",
5
5
  "author": {
6
6
  "name": "DeepVista AI",
@@ -78,6 +78,7 @@ Tunable via environment variables (read by the `SessionStart` hook):
78
78
  | `${CLAUDE_PLUGIN_ROOT}/agents/dv-<role>.md` | Generated subagent (one per managed-agent role) |
79
79
  | `~/.config/deepvista/catalog-state.json` | Last skill-sync timestamp + stub inventory |
80
80
  | `~/.config/deepvista/agent-defs-state.json` | Last agent-export timestamp + definition inventory |
81
+ | `~/.config/deepvista/config.json` | CLI profiles + top-level `session_skip_cwd_patterns` list |
81
82
  | `~/.config/deepvista/cache/skill-bodies/` | 5-minute TTL cache of fetched bodies |
82
83
  | `~/.config/deepvista/logs/catalog-sync.log` | Skill-sync hook stdout/stderr |
83
84
  | `~/.config/deepvista/logs/agent-export.log` | Agent-export hook stdout/stderr |
@@ -102,6 +103,24 @@ DEEPVISTA_FORCE_SYNC=1 deepvista skill sync --force
102
103
  deepvista agents export --force
103
104
  ```
104
105
 
106
+ Nested observer/sub-agent sessions showing up in the vistabase (e.g.
107
+ `observer-sessions · <hash>` notes from claude-mem's observer sub-claude)?
108
+ The plugin's SessionStart hook now skips CWDs matching the top-level
109
+ `session_skip_cwd_patterns` list in `~/.config/deepvista/config.json`.
110
+ Defaults cover `~/.claude-mem/observer-sessions`; to extend, edit the file
111
+ and add patterns (fnmatch-style globs):
112
+
113
+ ```jsonc
114
+ {
115
+ "default": { "api_url": "https://api.deepvista.ai" },
116
+ "session_skip_cwd_patterns": [
117
+ "*/.claude-mem/observer-sessions",
118
+ "*/.claude-mem/observer-sessions/*",
119
+ "*/scratchpad/*"
120
+ ]
121
+ }
122
+ ```
123
+
105
124
  No agents showing up as `@<role>`? Confirm you have managed agents with roles:
106
125
 
107
126
  ```
@@ -0,0 +1,129 @@
1
+ ---
2
+ description: DeepVista controls — `run` generates + dispatches today's planning note via the daily-planning skill; no args shows help
3
+ argument-hint: "[run]"
4
+ ---
5
+
6
+ DeepVista control surface. Behaviour depends on `$ARGUMENTS`:
7
+
8
+ - **No argument** (or any value other than `run`) → print the help block below.
9
+ - **`run`** → generate today's *Daily Planning* note via the `daily-planning`
10
+ skill (if one doesn't already exist), dispatch each `## <role>` section to
11
+ its matching `@<role>` subagent, and append a consolidated summary back
12
+ onto the note.
13
+
14
+ Planning notes are stored as regular DeepVista notes (`type=note`) tagged
15
+ ``daily-planning`` + ``date:YYYYMMDD``. No dedicated `deepvista planning`
16
+ CLI command exists — read/write everything via `deepvista notes` and
17
+ `deepvista card +search`.
18
+
19
+ ---
20
+
21
+ ## If `$ARGUMENTS` is empty or not `run`
22
+
23
+ Print this verbatim, then stop:
24
+
25
+ > **DeepVista — Claude Code commands**
26
+ >
27
+ > - `/deepvista run` — generate today's *Daily Planning* note (LLM-reasoned,
28
+ > driven by the `daily-planning` skill: yesterday's progress + last 7 days
29
+ > of cards → per-role tasks), then dispatch each `## <role>` section to
30
+ > the matching `@<role>` subagent. Subagent results are appended back to
31
+ > the planning note under a `## Summary — <timestamp>` block.
32
+ > - `/refresh-skills` — resync the DeepVista skill catalog and agent
33
+ > definitions immediately (bypasses the 60-minute throttle).
34
+ >
35
+ > **Tips**
36
+ >
37
+ > - Want to draft a plan without dispatching? Just say *"draft today's
38
+ > planning note"* and the `daily-planning` skill kicks in.
39
+ > - Personalise a subagent's voice by setting `config.system_prompt` on its
40
+ > managed agent (free text, e.g. *"You are the marketing specialist;
41
+ > follow persona context card persona-mkt-001."*) and re-running
42
+ > `/refresh-skills`. The agent loads the persona card at runtime.
43
+ > - Need help with the CLI itself? `deepvista --help` or `deepvista <group> --help`.
44
+
45
+ ## If `$ARGUMENTS` is `run`
46
+
47
+ Execute the daily-planning dispatch workflow.
48
+
49
+ ### Step 1 — Find today's planning note (or generate one)
50
+
51
+ ```bash
52
+ TODAY=$(date +%Y%m%d)
53
+ deepvista --format json card +search "Daily Planning $TODAY" --limit 5
54
+ ```
55
+
56
+ Walk the result and pick the card with both ``daily-planning`` and
57
+ ``date:$TODAY`` in `tags`. Two cases:
58
+
59
+ - **No match** → today's note doesn't exist. **Load the `daily-planning`
60
+ skill and follow it end-to-end** to produce today's plan. The skill ends
61
+ by saving the note via `deepvista notes create`. Re-run the search above
62
+ to pick up the new note id.
63
+
64
+ - **Match found** → fetch its full body:
65
+ ```bash
66
+ deepvista --format json notes get <note-id>
67
+ ```
68
+
69
+ Parse `description` and split on `## ` headings. Treat headings that match
70
+ ``Workflow today`` or ``Summary`` (case-insensitive) as reserved; everything
71
+ else is a role section keyed by its heading text (lowercased).
72
+
73
+ ### Step 2 — Dispatch each role section to its subagent
74
+
75
+ For each `(role, section_markdown)` in the role sections, invoke the
76
+ matching subagent inline. Skip any role with no on-disk `dv-<role>.md`
77
+ definition — the user hasn't registered a managed agent for it yet.
78
+ Example body:
79
+
80
+ ```
81
+ @<role>
82
+
83
+ You are dispatched from today's Daily Planning note (id: <note_id>).
84
+
85
+ Your section reads:
86
+
87
+ <section_markdown>
88
+
89
+ Complete it end-to-end. Return your deliverable in the standard subagent
90
+ output format (Frame → Deliverable → Sources → Captured).
91
+ ```
92
+
93
+ Collect each subagent's full reply.
94
+
95
+ ### Step 3 — Append a consolidated summary to the note
96
+
97
+ Build a single markdown block:
98
+
99
+ ```
100
+ ## Summary — <YYYY-MM-DD HH:MM>
101
+
102
+ ### @marketing
103
+ <that subagent's reply>
104
+
105
+ ### @engineering
106
+ <that subagent's reply>
107
+
108
+
109
+ ```
110
+
111
+ Read the current body, append the block, write it back:
112
+
113
+ ```bash
114
+ deepvista --format json notes get <note-id>
115
+ # Capture description from JSON, append the block, then:
116
+ deepvista notes update <note-id> --content-file -
117
+ # (pass `description + appended_block` on stdin)
118
+ ```
119
+
120
+ ### Step 4 — Report
121
+
122
+ Tell the user, in two lines:
123
+
124
+ - Whether the plan was generated this run (and via which skill), or pulled
125
+ from an existing note.
126
+ - Roles dispatched (and any skipped because no matching subagent existed),
127
+ plus the planning note URL (`https://app.deepvista.ai/notes/<id>`).
128
+
129
+ If any step fails, stop and surface the error — do not silently fall through.
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: daily-planning
3
+ description: |
4
+ Generate today's *Daily Planning* note by reading yesterday's plan (and its
5
+ appended progress summary) along with recent context cards from the last 7
6
+ days — then reason about what carries over, what's new, and what each
7
+ registered `@<role>` managed-agent subagent should own today. Save the
8
+ result as a regular DeepVista note tagged ``daily-planning`` +
9
+ ``date:YYYYMMDD``. Use when the user runs `/deepvista run` and today's
10
+ planning note doesn't yet exist, or when the user asks to "draft today's
11
+ plan", "make today's standup", or "regenerate my daily planning note".
12
+ ---
13
+
14
+ # Daily planning — generate today's plan from yesterday's context
15
+
16
+ This skill produces today's *Daily Planning* note as an LLM-reasoned plan,
17
+ not a static template. Planning notes are plain `deepvista notes` cards
18
+ (type=note) tagged ``daily-planning`` + ``date:YYYYMMDD`` — there is no
19
+ dedicated `deepvista planning` subcommand. The flow:
20
+
21
+ 1. Read **yesterday's** planning note + appended summary (if it exists).
22
+ 2. Read the **last 7 days** of context cards (notes, todos, sessions).
23
+ 3. List the **`@<role>` subagents** registered for this user.
24
+ 4. Reason about carry-over, new tasks, and blockers.
25
+ 5. Save the result as a note via `deepvista notes create`.
26
+
27
+ The CLI does the data lookups; this skill is the reasoning runbook.
28
+
29
+ ## Step 1 — Read yesterday's plan
30
+
31
+ Search for yesterday's planning note by tag (titles can be edited; tags
32
+ shouldn't). Hybrid search filtered to `type=note` is the most robust:
33
+
34
+ ```bash
35
+ YESTERDAY=$(date -v-1d +%Y%m%d 2>/dev/null || date -d 'yesterday' +%Y%m%d)
36
+ TODAY=$(date +%Y%m%d)
37
+
38
+ deepvista --format json card +search "Daily Planning $YESTERDAY" --limit 5
39
+ # If a match is found, fetch its full body:
40
+ # deepvista --format json notes get <id>
41
+ ```
42
+
43
+ Parse `description` (yesterday's full markdown — plan + any
44
+ `## Summary — <timestamp>` blocks that `/deepvista run` appended after
45
+ subagents finished). If nothing matches, treat yesterday as a clean slate.
46
+
47
+ ## Step 2 — Read the last 7 days of context cards
48
+
49
+ Each call is bounded (`--limit 20`). Skip silently on failure.
50
+
51
+ ```bash
52
+ deepvista --format json notes list --limit 20
53
+ deepvista --format json card list --type todo --limit 20
54
+ deepvista --format json card list --type session --limit 10
55
+ deepvista --format json card +search "progress OR blocker OR shipped OR next" --limit 10
56
+ ```
57
+
58
+ For each result, keep only items updated in the last 7 days. Extract:
59
+
60
+ - title
61
+ - last-updated date
62
+ - a one-sentence read of what it implies for today
63
+
64
+ ## Step 3 — List the registered `@<role>` subagents
65
+
66
+ ```bash
67
+ ls "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/deepvista}/agents/" 2>/dev/null \
68
+ | grep -E '^dv-.*\.md$' \
69
+ | sed 's/^dv-//; s/\.md$//'
70
+ ```
71
+
72
+ If the list is empty, default to `marketing,engineering,gtm`. The user can
73
+ register more via `deepvista agents register`.
74
+
75
+ ## Step 4 — Reason about today's plan
76
+
77
+ You now have:
78
+
79
+ - Yesterday's plan + appended summaries (assigned vs. shipped).
80
+ - A 7-day rolling window of notes/todos/sessions (the *why* behind today).
81
+ - The list of available `@<role>` subagents (the *who* of today).
82
+
83
+ Produce markdown that:
84
+
85
+ 1. **Opens with a 2-3 sentence preamble** restating the week's arc — what's
86
+ in progress, what shipped yesterday, what's next.
87
+ 2. **`## Workflow today`** — a short bulleted list of cross-cutting work the
88
+ *main agent* will run directly (not delegated). Include `/refresh-skills`
89
+ if subagents are stale; surface explicit user todos; surface blockers.
90
+ 3. **One `## <role>` section per registered subagent**, each with:
91
+ - **1 must-do** — finishable today, traceable to a card or yesterday's
92
+ summary. Reference the source card id inline (e.g. `(see card-xyz)`).
93
+ - **0–2 stretch goals** — only if there's spare bandwidth.
94
+ - **Blockers** — call out anything the role can't proceed without.
95
+ 4. **`## Summary`** — leave empty (`_Subagent results land here after
96
+ `/deepvista run` finishes._`). The `/deepvista run` flow fills it later.
97
+
98
+ Hard constraints:
99
+
100
+ - Total length ≤ 600 words. Brevity beats completeness for a daily plan.
101
+ - Every task must trace to a real card, note, or yesterday's summary —
102
+ never invent work. If a role has nothing genuinely ready, write
103
+ `- _No queued work today — the {role} specialist is free for ad-hoc
104
+ requests._` and move on.
105
+ - Use the section headers (`## Workflow today`, `## <role>`, `## Summary`)
106
+ exactly — the `/deepvista run` slash command parses them by string match.
107
+
108
+ ## Step 5 — Save the plan as a DeepVista note
109
+
110
+ Pipe the markdown straight into `deepvista notes create` and tag it so
111
+ tomorrow's run finds it:
112
+
113
+ ```bash
114
+ cat <<'PLAN' | deepvista notes create \
115
+ --title "Daily Planning $TODAY" \
116
+ --content-file - \
117
+ --tags "[\"daily-planning\",\"date:$TODAY\",\"source:agent\"]"
118
+ # Daily Planning $TODAY
119
+
120
+ <preamble>
121
+
122
+ ## Workflow today
123
+ - …
124
+
125
+ ## marketing
126
+ - …
127
+
128
+ ## engineering
129
+ - …
130
+
131
+ ## Summary
132
+
133
+ _Subagent results land here after `/deepvista run` finishes._
134
+ PLAN
135
+ ```
136
+
137
+ Confirm with the user before running the save (this is a write command).
138
+ Surface the note URL — `https://app.deepvista.ai/notes/<id>` — in the
139
+ response so they can edit any section before `/deepvista run` dispatches it.
140
+
141
+ ## Append a summary after `/deepvista run` finishes
142
+
143
+ The slash command appends each subagent's reply onto today's note. There is
144
+ no dedicated `append-summary` CLI — use `deepvista notes update`:
145
+
146
+ ```bash
147
+ deepvista notes get <note-id> # read current description
148
+ # build new_description = current + "\n\n## Summary — <timestamp>\n<consolidated block>\n"
149
+ deepvista notes update <note-id> --content-file - # pipe new_description on stdin
150
+ ```
151
+
152
+ ## Quick reference
153
+
154
+ | Command | Reads | Writes |
155
+ |---|---|---|
156
+ | `card +search "Daily Planning <date>"` | semantic search across cards | — |
157
+ | `notes get <id>` | one note's full body | — |
158
+ | `notes list --limit 20` | recent notes | — |
159
+ | `card list --type todo` | recent todos | — |
160
+ | `notes create --title ... --content-file - --tags ...` | stdin markdown | new note |
161
+ | `notes update <id> --content-file -` | stdin markdown | replaces the note body |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepvista-cli"
3
- version = "0.1.18"
3
+ version = "0.2.0"
4
4
  description = "CLI for DeepVista — chat, notes, skills, and memory from your terminal."
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -54,6 +54,37 @@ deepvista session tick \
54
54
  deepvista session finalize --session-id "$CLAUDE_SESSION_ID"
55
55
  ```
56
56
 
57
+ ## Skipping nested sub-agent sessions
58
+
59
+ `session init` short-circuits when `--cwd` matches any pattern in
60
+ `session_skip_cwd_patterns` (a top-level list in
61
+ `~/.config/deepvista/config.json`). This stops the SessionStart hook from
62
+ recording sub-agent sessions whose CWD is not a real working directory — e.g.
63
+ **claude-mem's observer sub-claude** runs with
64
+ `cwd=~/.claude-mem/observer-sessions` and only quotes file paths from the
65
+ *primary* session; recording it pollutes the vistabase with bogus File cards.
66
+
67
+ Defaults skip `*/.claude-mem/observer-sessions[/*]`. Patterns are
68
+ fnmatch-style Unix shell globs (`*` matches across `/`). To override, edit
69
+ `~/.config/deepvista/config.json` by hand — add a top-level
70
+ `session_skip_cwd_patterns` list. An explicit empty list disables skipping.
71
+
72
+ ```jsonc
73
+ {
74
+ "default": { "api_url": "https://api.deepvista.ai" },
75
+ "session_skip_cwd_patterns": [
76
+ "*/.claude-mem/observer-sessions",
77
+ "*/.claude-mem/observer-sessions/*",
78
+ "*/scratchpad/*"
79
+ ]
80
+ }
81
+ ```
82
+
83
+ When a session is skipped, `init` emits
84
+ `{"skipped": true, "reason": "cwd matches session_skip_cwd_patterns", ...}`
85
+ and creates no card. Downstream `tick` / `finalize` self-no-op because no
86
+ card was cached (exit 3 "Unknown session", silenced by the hook scripts).
87
+
57
88
  ## See also
58
89
 
59
90
  - [notes.md](notes.md) — explicit user-authored knowledge.
@@ -56,7 +56,7 @@ wheels = [
56
56
 
57
57
  [[package]]
58
58
  name = "deepvista-cli"
59
- version = "0.1.18"
59
+ version = "0.2.0"
60
60
  source = { editable = "." }
61
61
  dependencies = [
62
62
  { name = "click" },
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.1.18"
3
- }
@@ -1,225 +0,0 @@
1
- """Tests for the managed-agent → Claude Code subagent export pipeline (DV-836)."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from deepvista_cli import agent_catalog
10
-
11
- # ---------------------------------------------------------------------------
12
- # Fake client
13
- # ---------------------------------------------------------------------------
14
-
15
-
16
- class FakeClient:
17
- """In-memory stand-in for ``DeepVistaClient`` (GET only)."""
18
-
19
- def __init__(self, agents: list[dict[str, Any]]) -> None:
20
- self._agents = agents
21
- self.calls: list[tuple[str, dict | None]] = []
22
-
23
- def get(self, path: str, params: dict | None = None) -> Any:
24
- self.calls.append((path, params))
25
- if path == "/agents":
26
- return {"agents": self._agents}
27
- raise AssertionError(f"unexpected GET {path}")
28
-
29
-
30
- def _agent(role: str, *, name: str = "", aid: str = "id-x", updated: str = "2026-01-01") -> dict[str, Any]:
31
- return {
32
- "id": aid,
33
- "name": name or f"{role.title()} Claude",
34
- "agent_type": "claude-code",
35
- "agent_role": role,
36
- "updated_at": updated,
37
- }
38
-
39
-
40
- # ---------------------------------------------------------------------------
41
- # slug / render
42
- # ---------------------------------------------------------------------------
43
-
44
-
45
- def test_slugify_basic():
46
- assert agent_catalog.slugify("Marketing") == "marketing"
47
- assert agent_catalog.slugify("Go To Market!") == "go-to-market"
48
-
49
-
50
- def test_slugify_fallback_on_empty():
51
- assert agent_catalog.slugify("你好", fallback="fallback") == "fallback"
52
-
53
-
54
- def test_file_name_applies_prefix():
55
- meta = agent_catalog.AgentRoleMeta(role="marketing", agent_name="M", agent_id="a")
56
- assert agent_catalog.file_name(meta, prefix="dv-") == "dv-marketing.md"
57
-
58
-
59
- def test_build_agent_markdown_handle_and_marker():
60
- meta = agent_catalog.AgentRoleMeta(
61
- role="marketing", agent_name="Marketing Claude", agent_id="abc123", updated_at="2026-05-01"
62
- )
63
- md = agent_catalog.build_agent_markdown(meta)
64
- # Invocation handle is the bare role, so `@marketing` resolves.
65
- assert "name: marketing" in md
66
- # Carries our marker + provenance so we can safely reclaim it later.
67
- assert f"{agent_catalog.AGENT_MARKER}: true" in md
68
- assert "x-deepvista-role:" in md
69
- assert "x-deepvista-agent-id: abc123" in md
70
- # Known role gets its specialist description + preloads the deepvista skill.
71
- assert "Marketing specialist for DeepVista" in md
72
- assert "skills: deepvista" in md
73
- assert "Marketing Claude" in md # backing managed-agent name in the body
74
-
75
-
76
- def test_build_agent_markdown_generic_role():
77
- meta = agent_catalog.AgentRoleMeta(role="legal", agent_name="", agent_id="z")
78
- md = agent_catalog.build_agent_markdown(meta)
79
- assert "name: legal" in md
80
- assert "Legal specialist for DeepVista" in md
81
-
82
-
83
- def test_build_agent_markdown_bakes_custom_soul():
84
- """A managed agent with config.soul renders that prompt as the body, not the template."""
85
- meta = agent_catalog.AgentRoleMeta(
86
- role="marketing",
87
- agent_name="Marketing",
88
- agent_id="abc123",
89
- system_prompt="You are a bespoke marketing brain.\nGround everything in notes.",
90
- )
91
- md = agent_catalog.build_agent_markdown(meta)
92
- # Frontmatter stays templated (routing + preloaded skill + role description).
93
- assert "name: marketing" in md
94
- assert "skills: deepvista" in md
95
- assert "Marketing specialist for DeepVista" in md
96
- # Body is the custom prompt verbatim; the default template body is bypassed.
97
- assert "You are a bespoke marketing brain." in md
98
- assert "Operating procedure" not in md
99
-
100
-
101
- def test_metas_from_agents_reads_config_soul():
102
- """config.soul flows into AgentRoleMeta.system_prompt for the export to bake in."""
103
- agents = [
104
- {
105
- "id": "m1",
106
- "name": "Marketing",
107
- "agent_type": "deepvista-cli",
108
- "agent_role": "marketing",
109
- "updated_at": "2026-05-27",
110
- "config": {"soul": "CUSTOM PROMPT", "machine": "laptop"},
111
- }
112
- ]
113
- metas = agent_catalog.metas_from_agents(agents)
114
- assert len(metas) == 1
115
- assert metas[0].system_prompt == "CUSTOM PROMPT"
116
-
117
-
118
- # ---------------------------------------------------------------------------
119
- # grouping
120
- # ---------------------------------------------------------------------------
121
-
122
-
123
- def test_metas_skip_misc_and_empty():
124
- metas = agent_catalog.metas_from_agents([_agent("misc"), _agent(""), _agent(" "), _agent("sales")])
125
- assert [m.role for m in metas] == ["sales"]
126
-
127
-
128
- def test_metas_group_by_role_keep_freshest_and_count():
129
- metas = agent_catalog.metas_from_agents(
130
- [
131
- _agent("sales", name="Old Sales", aid="a1", updated="2026-01-01"),
132
- _agent("sales", name="New Sales", aid="a2", updated="2026-05-01"),
133
- _agent("marketing", aid="b1", updated="2026-02-01"),
134
- ]
135
- )
136
- by_role = {m.role: m for m in metas}
137
- assert set(by_role) == {"sales", "marketing"}
138
- assert by_role["sales"].agent_id == "a2" # freshest representative
139
- assert by_role["sales"].agent_name == "New Sales"
140
- assert by_role["sales"].count == 2
141
-
142
-
143
- # ---------------------------------------------------------------------------
144
- # curated detection
145
- # ---------------------------------------------------------------------------
146
-
147
-
148
- def test_curated_names_ignores_our_files(tmp_path: Path):
149
- # A hand-curated agent (no marker).
150
- (tmp_path / "marketing.md").write_text("---\nname: marketing\n---\nbody\n", encoding="utf-8")
151
- # One of ours (carries the marker) — must not count as curated.
152
- (tmp_path / "dv-sales.md").write_text(
153
- f"---\nname: sales\n{agent_catalog.AGENT_MARKER}: true\n---\n", encoding="utf-8"
154
- )
155
- assert agent_catalog.curated_names(tmp_path, "dv-") == {"marketing"}
156
-
157
-
158
- def test_plan_skips_curated_role(tmp_path: Path):
159
- (tmp_path / "marketing.md").write_text("---\nname: marketing\n---\n", encoding="utf-8")
160
- metas = agent_catalog.metas_from_agents([_agent("marketing"), _agent("sales")])
161
- plan = agent_catalog.compute_plan(metas, target=tmp_path, prefix="dv-", state={})
162
- assert plan.skipped_curated == ["marketing"]
163
- assert [m.slug for m in plan.to_add] == ["sales"]
164
-
165
-
166
- # ---------------------------------------------------------------------------
167
- # plan + apply + end-to-end
168
- # ---------------------------------------------------------------------------
169
-
170
-
171
- def test_sync_writes_then_idempotent(tmp_path: Path):
172
- client = FakeClient([_agent("sales"), _agent("marketing")])
173
- res = agent_catalog.sync_agent_defs(client, target=tmp_path, force=True, state_path=tmp_path / "state.json")
174
- assert res["ok"] is True
175
- assert res["summary"]["added"] == 2
176
- assert (tmp_path / "dv-sales.md").exists()
177
- assert (tmp_path / "dv-marketing.md").exists()
178
-
179
- # Re-run: nothing changes.
180
- res2 = agent_catalog.sync_agent_defs(client, target=tmp_path, force=True, state_path=tmp_path / "state.json")
181
- assert res2["summary"] == {"added": 0, "updated": 0, "removed": 0, "unchanged": 2, "skipped_curated": 0}
182
-
183
-
184
- def test_sync_removes_def_when_role_gone(tmp_path: Path):
185
- state = tmp_path / "state.json"
186
- agent_catalog.sync_agent_defs(
187
- FakeClient([_agent("sales"), _agent("marketing")]), target=tmp_path, force=True, state_path=state
188
- )
189
- assert (tmp_path / "dv-marketing.md").exists()
190
-
191
- # Marketing role disappears server-side → its def is reclaimed.
192
- res = agent_catalog.sync_agent_defs(FakeClient([_agent("sales")]), target=tmp_path, force=True, state_path=state)
193
- assert res["summary"]["removed"] == 1
194
- assert not (tmp_path / "dv-marketing.md").exists()
195
- assert (tmp_path / "dv-sales.md").exists()
196
-
197
-
198
- def test_sync_never_deletes_unmarked_file(tmp_path: Path):
199
- state = tmp_path / "state.json"
200
- agent_catalog.sync_agent_defs(FakeClient([_agent("sales")]), target=tmp_path, force=True, state_path=state)
201
- # A file sharing our prefix but without the marker must survive a reclaim.
202
- intruder = tmp_path / "dv-sales.md"
203
- intruder.write_text("---\nname: sales\n---\nhand edited\n", encoding="utf-8")
204
- agent_catalog.sync_agent_defs(FakeClient([]), target=tmp_path, force=True, state_path=state)
205
- assert intruder.exists()
206
- assert "hand edited" in intruder.read_text(encoding="utf-8")
207
-
208
-
209
- def test_sync_throttled_skips_network(tmp_path: Path):
210
- state = tmp_path / "state.json"
211
- agent_catalog.save_state({"last_sync_epoch": int(time.time()), "defs": []}, state)
212
- client = FakeClient([_agent("sales")])
213
- res = agent_catalog.sync_agent_defs(client, target=tmp_path, throttle_min=60, state_path=state)
214
- assert res["skipped"] == "throttled"
215
- assert client.calls == [] # no network hit
216
-
217
-
218
- def test_dry_run_writes_nothing(tmp_path: Path):
219
- client = FakeClient([_agent("sales")])
220
- res = agent_catalog.sync_agent_defs(
221
- client, target=tmp_path, force=True, dry_run=True, state_path=tmp_path / "s.json"
222
- )
223
- assert res["dry_run"] is True
224
- assert res["plan"]["to_add"] == ["sales"]
225
- assert not list(tmp_path.glob("dv-*.md"))
File without changes
File without changes
File without changes
File without changes
File without changes