intent-cli-python 0.6.0__tar.gz → 1.0.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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 0.6.0
3
+ Version: 1.0.0
4
4
  Summary: Semantic history for agent-driven development. Records what you did and why.
5
5
  Author: Zeng Deyang
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/dozybot001/Intent
8
8
  Project-URL: Repository, https://github.com/dozybot001/Intent
9
9
  Keywords: agent,git,semantic-history,intent,developer-tools
10
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Programming Language :: Python :: 3
@@ -40,14 +40,34 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
40
40
 
41
41
  Objects link automatically: creating an intent attaches all active decisions; creating a decision attaches all active intents. Relationships are always bidirectional and append-only.
42
42
 
43
+ ### How decisions are created
44
+
45
+ Decisions require human involvement. Two paths:
46
+
47
+ - **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
48
+ - **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
49
+
43
50
  ## Install
44
51
 
45
52
  ```bash
53
+ # Clone the repository
54
+ git clone https://github.com/dozybot001/Intent.git
55
+
56
+ # Install the CLI (pipx recommended)
57
+ pipx install intent-cli-python
58
+
59
+ # Or using pip
46
60
  pip install intent-cli-python
47
61
  ```
48
62
 
49
63
  Requires Python 3.9+ and Git.
50
64
 
65
+ ### Add the Claude Code skill
66
+
67
+ ```bash
68
+ npx skills add dozybot001/Intent
69
+ ```
70
+
51
71
  ## Quick start
52
72
 
53
73
  ```bash
@@ -136,6 +156,12 @@ All data lives in `.intent/` at your git repo root:
136
156
  decision-001.json
137
157
  ```
138
158
 
159
+ ## Docs
160
+
161
+ - [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
162
+ - [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
163
+ - [Roadmap](docs/EN/roadmap.md) — phase plan
164
+
139
165
  ## License
140
166
 
141
167
  MIT
@@ -16,14 +16,34 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
16
16
 
17
17
  Objects link automatically: creating an intent attaches all active decisions; creating a decision attaches all active intents. Relationships are always bidirectional and append-only.
18
18
 
19
+ ### How decisions are created
20
+
21
+ Decisions require human involvement. Two paths:
22
+
23
+ - **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
24
+ - **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
25
+
19
26
  ## Install
20
27
 
21
28
  ```bash
29
+ # Clone the repository
30
+ git clone https://github.com/dozybot001/Intent.git
31
+
32
+ # Install the CLI (pipx recommended)
33
+ pipx install intent-cli-python
34
+
35
+ # Or using pip
22
36
  pip install intent-cli-python
23
37
  ```
24
38
 
25
39
  Requires Python 3.9+ and Git.
26
40
 
41
+ ### Add the Claude Code skill
42
+
43
+ ```bash
44
+ npx skills add dozybot001/Intent
45
+ ```
46
+
27
47
  ## Quick start
28
48
 
29
49
  ```bash
@@ -112,6 +132,12 @@ All data lives in `.intent/` at your git repo root:
112
132
  decision-001.json
113
133
  ```
114
134
 
135
+ ## Docs
136
+
137
+ - [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
138
+ - [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
139
+ - [Roadmap](docs/EN/roadmap.md) — phase plan
140
+
115
141
  ## License
116
142
 
117
143
  MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intent-cli-python"
7
- version = "0.6.0"
7
+ version = "1.0.0"
8
8
  description = "Semantic history for agent-driven development. Records what you did and why."
9
9
  requires-python = ">=3.9"
10
10
  readme = "README.md"
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  keywords = ["agent", "git", "semantic-history", "intent", "developer-tools"]
16
16
  classifiers = [
17
- "Development Status :: 4 - Beta",
17
+ "Development Status :: 5 - Production/Stable",
18
18
  "Environment :: Console",
19
19
  "Intended Audience :: Developers",
20
20
  "Programming Language :: Python :: 3",
@@ -11,7 +11,7 @@ from intent_cli.store import (
11
11
  next_id, read_object, write_object, list_objects, read_config,
12
12
  )
13
13
 
14
- VERSION = "0.6.0"
14
+ VERSION = "1.0.0"
15
15
 
16
16
 
17
17
  def _now():
@@ -98,7 +98,7 @@ def cmd_inspect(_args):
98
98
 
99
99
  print(json.dumps({
100
100
  "ok": True,
101
- "schema_version": config.get("schema_version", "0.6"),
101
+ "schema_version": config.get("schema_version", "1.0"),
102
102
  "active_intents": active_intents,
103
103
  "suspend_intents": suspend_intents,
104
104
  "active_decisions": active_decisions,
@@ -129,6 +129,7 @@ def cmd_intent_create(args):
129
129
  "title": args.title,
130
130
  "status": "active",
131
131
  "source_query": args.query,
132
+ "rationale": args.rationale,
132
133
  "decision_ids": decision_ids,
133
134
  "snap_ids": [],
134
135
  }
@@ -234,6 +235,7 @@ def cmd_snap_create(args):
234
235
  "status": "active",
235
236
  "intent_id": intent_id,
236
237
  "query": args.query,
238
+ "rationale": args.rationale,
237
239
  "summary": args.summary,
238
240
  "feedback": args.feedback,
239
241
  }
@@ -390,6 +392,7 @@ def main():
390
392
  p = s_intent.add_parser("create")
391
393
  p.add_argument("title")
392
394
  p.add_argument("--query", default="")
395
+ p.add_argument("--rationale", default="")
393
396
 
394
397
  p = s_intent.add_parser("list")
395
398
  p.add_argument("--status", default=None)
@@ -414,6 +417,7 @@ def main():
414
417
  p.add_argument("title")
415
418
  p.add_argument("--intent", required=True)
416
419
  p.add_argument("--query", default="")
420
+ p.add_argument("--rationale", default="")
417
421
  p.add_argument("--summary", default="")
418
422
  p.add_argument("--feedback", default="")
419
423
 
@@ -43,7 +43,7 @@ def init_workspace():
43
43
  d.mkdir()
44
44
  for sub in SUBDIRS.values():
45
45
  (d / sub).mkdir()
46
- (d / "config.json").write_text(json.dumps({"schema_version": "0.6"}, indent=2))
46
+ (d / "config.json").write_text(json.dumps({"schema_version": "1.0"}, indent=2))
47
47
  return d, None
48
48
 
49
49
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 0.6.0
3
+ Version: 1.0.0
4
4
  Summary: Semantic history for agent-driven development. Records what you did and why.
5
5
  Author: Zeng Deyang
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/dozybot001/Intent
8
8
  Project-URL: Repository, https://github.com/dozybot001/Intent
9
9
  Keywords: agent,git,semantic-history,intent,developer-tools
10
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Programming Language :: Python :: 3
@@ -40,14 +40,34 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
40
40
 
41
41
  Objects link automatically: creating an intent attaches all active decisions; creating a decision attaches all active intents. Relationships are always bidirectional and append-only.
42
42
 
43
+ ### How decisions are created
44
+
45
+ Decisions require human involvement. Two paths:
46
+
47
+ - **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
48
+ - **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
49
+
43
50
  ## Install
44
51
 
45
52
  ```bash
53
+ # Clone the repository
54
+ git clone https://github.com/dozybot001/Intent.git
55
+
56
+ # Install the CLI (pipx recommended)
57
+ pipx install intent-cli-python
58
+
59
+ # Or using pip
46
60
  pip install intent-cli-python
47
61
  ```
48
62
 
49
63
  Requires Python 3.9+ and Git.
50
64
 
65
+ ### Add the Claude Code skill
66
+
67
+ ```bash
68
+ npx skills add dozybot001/Intent
69
+ ```
70
+
51
71
  ## Quick start
52
72
 
53
73
  ```bash
@@ -136,6 +156,12 @@ All data lives in `.intent/` at your git repo root:
136
156
  decision-001.json
137
157
  ```
138
158
 
159
+ ## Docs
160
+
161
+ - [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
162
+ - [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
163
+ - [Roadmap](docs/EN/roadmap.md) — phase plan
164
+
139
165
  ## License
140
166
 
141
167
  MIT
@@ -10,4 +10,5 @@ src/intent_cli_python.egg-info/PKG-INFO
10
10
  src/intent_cli_python.egg-info/SOURCES.txt
11
11
  src/intent_cli_python.egg-info/dependency_links.txt
12
12
  src/intent_cli_python.egg-info/entry_points.txt
13
- src/intent_cli_python.egg-info/top_level.txt
13
+ src/intent_cli_python.egg-info/top_level.txt
14
+ tests/test_cli.py
@@ -0,0 +1,304 @@
1
+ """Tests for Intent CLI — covers all 19 commands, state machines, and error codes."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+
12
+ @pytest.fixture
13
+ def workspace(tmp_path):
14
+ """Create a git repo with .intent/ initialized."""
15
+ subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
16
+ subprocess.run(
17
+ ["git", "commit", "--allow-empty", "-m", "init"],
18
+ cwd=tmp_path, capture_output=True, check=True,
19
+ )
20
+ result = _run(tmp_path, "init")
21
+ assert result["ok"] is True
22
+ return tmp_path
23
+
24
+
25
+ def _run(cwd, *args):
26
+ """Run itt command and return parsed JSON."""
27
+ r = subprocess.run(
28
+ ["itt", *args],
29
+ cwd=cwd, capture_output=True, text=True,
30
+ )
31
+ return json.loads(r.stdout)
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Global commands
36
+ # ---------------------------------------------------------------------------
37
+
38
+ class TestGlobal:
39
+ def test_version(self, workspace):
40
+ r = _run(workspace, "version")
41
+ assert r["ok"] is True
42
+ assert "version" in r["result"]
43
+
44
+ def test_init_already_exists(self, workspace):
45
+ r = _run(workspace, "init")
46
+ assert r["ok"] is False
47
+ assert r["error"]["code"] == "ALREADY_EXISTS"
48
+
49
+ def test_init_not_git(self, tmp_path):
50
+ r = _run(tmp_path, "init")
51
+ assert r["ok"] is False
52
+ assert r["error"]["code"] == "GIT_STATE_INVALID"
53
+
54
+ def test_inspect_empty(self, workspace):
55
+ r = _run(workspace, "inspect")
56
+ assert r["ok"] is True
57
+ assert r["active_intents"] == []
58
+ assert r["active_decisions"] == []
59
+ assert r["recent_snaps"] == []
60
+
61
+ def test_not_initialized(self, tmp_path):
62
+ subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
63
+ subprocess.run(
64
+ ["git", "commit", "--allow-empty", "-m", "init"],
65
+ cwd=tmp_path, capture_output=True, check=True,
66
+ )
67
+ r = _run(tmp_path, "inspect")
68
+ assert r["ok"] is False
69
+ assert r["error"]["code"] == "NOT_INITIALIZED"
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Intent commands
74
+ # ---------------------------------------------------------------------------
75
+
76
+ class TestIntent:
77
+ def test_create(self, workspace):
78
+ r = _run(workspace, "intent", "create", "Fix bug", "--query", "why crash?")
79
+ assert r["ok"] is True
80
+ assert r["result"]["id"] == "intent-001"
81
+ assert r["result"]["status"] == "active"
82
+ assert r["result"]["source_query"] == "why crash?"
83
+
84
+ def test_create_auto_attaches_decisions(self, workspace):
85
+ _run(workspace, "intent", "create", "Goal A", "--query", "q")
86
+ _run(workspace, "decision", "create", "Rule 1", "--rationale", "r")
87
+ r = _run(workspace, "intent", "create", "Goal B", "--query", "q")
88
+ assert "decision-001" in r["result"]["decision_ids"]
89
+
90
+ def test_list(self, workspace):
91
+ _run(workspace, "intent", "create", "A", "--query", "q")
92
+ _run(workspace, "intent", "create", "B", "--query", "q")
93
+ r = _run(workspace, "intent", "list")
94
+ assert len(r["result"]) == 2
95
+
96
+ def test_list_filter_status(self, workspace):
97
+ _run(workspace, "intent", "create", "A", "--query", "q")
98
+ _run(workspace, "intent", "create", "B", "--query", "q")
99
+ _run(workspace, "intent", "suspend", "intent-001")
100
+ r = _run(workspace, "intent", "list", "--status", "active")
101
+ assert len(r["result"]) == 1
102
+ assert r["result"][0]["id"] == "intent-002"
103
+
104
+ def test_show(self, workspace):
105
+ _run(workspace, "intent", "create", "A", "--query", "q")
106
+ r = _run(workspace, "intent", "show", "intent-001")
107
+ assert r["result"]["title"] == "A"
108
+
109
+ def test_show_not_found(self, workspace):
110
+ r = _run(workspace, "intent", "show", "intent-999")
111
+ assert r["error"]["code"] == "OBJECT_NOT_FOUND"
112
+
113
+ def test_suspend_activate(self, workspace):
114
+ _run(workspace, "intent", "create", "A", "--query", "q")
115
+ r = _run(workspace, "intent", "suspend", "intent-001")
116
+ assert r["result"]["status"] == "suspend"
117
+ r = _run(workspace, "intent", "activate", "intent-001")
118
+ assert r["result"]["status"] == "active"
119
+
120
+ def test_activate_catches_up_decisions(self, workspace):
121
+ _run(workspace, "intent", "create", "A", "--query", "q")
122
+ _run(workspace, "intent", "suspend", "intent-001")
123
+ _run(workspace, "decision", "create", "New rule", "--rationale", "r")
124
+ r = _run(workspace, "intent", "activate", "intent-001")
125
+ assert "decision-001" in r["result"]["decision_ids"]
126
+
127
+ def test_done(self, workspace):
128
+ _run(workspace, "intent", "create", "A", "--query", "q")
129
+ r = _run(workspace, "intent", "done", "intent-001")
130
+ assert r["result"]["status"] == "done"
131
+
132
+ def test_done_is_terminal(self, workspace):
133
+ _run(workspace, "intent", "create", "A", "--query", "q")
134
+ _run(workspace, "intent", "done", "intent-001")
135
+ r = _run(workspace, "intent", "activate", "intent-001")
136
+ assert r["error"]["code"] == "STATE_CONFLICT"
137
+
138
+ def test_suspend_only_active(self, workspace):
139
+ _run(workspace, "intent", "create", "A", "--query", "q")
140
+ _run(workspace, "intent", "done", "intent-001")
141
+ r = _run(workspace, "intent", "suspend", "intent-001")
142
+ assert r["error"]["code"] == "STATE_CONFLICT"
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Snap commands
147
+ # ---------------------------------------------------------------------------
148
+
149
+ class TestSnap:
150
+ def test_create(self, workspace):
151
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
152
+ r = _run(workspace, "snap", "create", "Did X", "--intent", "intent-001",
153
+ "--summary", "details")
154
+ assert r["ok"] is True
155
+ assert r["result"]["id"] == "snap-001"
156
+ assert r["result"]["intent_id"] == "intent-001"
157
+
158
+ def test_create_updates_intent_snap_ids(self, workspace):
159
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
160
+ _run(workspace, "snap", "create", "S1", "--intent", "intent-001")
161
+ _run(workspace, "snap", "create", "S2", "--intent", "intent-001")
162
+ r = _run(workspace, "intent", "show", "intent-001")
163
+ assert r["result"]["snap_ids"] == ["snap-001", "snap-002"]
164
+
165
+ def test_create_requires_active_intent(self, workspace):
166
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
167
+ _run(workspace, "intent", "done", "intent-001")
168
+ r = _run(workspace, "snap", "create", "S", "--intent", "intent-001")
169
+ assert r["error"]["code"] == "STATE_CONFLICT"
170
+
171
+ def test_create_intent_not_found(self, workspace):
172
+ r = _run(workspace, "snap", "create", "S", "--intent", "intent-999")
173
+ assert r["error"]["code"] == "OBJECT_NOT_FOUND"
174
+
175
+ def test_list(self, workspace):
176
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
177
+ _run(workspace, "snap", "create", "S1", "--intent", "intent-001")
178
+ _run(workspace, "snap", "create", "S2", "--intent", "intent-001")
179
+ r = _run(workspace, "snap", "list")
180
+ assert len(r["result"]) == 2
181
+
182
+ def test_list_filter_intent(self, workspace):
183
+ _run(workspace, "intent", "create", "A", "--query", "q")
184
+ _run(workspace, "intent", "create", "B", "--query", "q")
185
+ _run(workspace, "snap", "create", "S1", "--intent", "intent-001")
186
+ _run(workspace, "snap", "create", "S2", "--intent", "intent-002")
187
+ r = _run(workspace, "snap", "list", "--intent", "intent-002")
188
+ assert len(r["result"]) == 1
189
+ assert r["result"][0]["id"] == "snap-002"
190
+
191
+ def test_feedback(self, workspace):
192
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
193
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
194
+ r = _run(workspace, "snap", "feedback", "snap-001", "looks good")
195
+ assert r["result"]["feedback"] == "looks good"
196
+
197
+ def test_feedback_overwrites(self, workspace):
198
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
199
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
200
+ _run(workspace, "snap", "feedback", "snap-001", "first")
201
+ r = _run(workspace, "snap", "feedback", "snap-001", "second")
202
+ assert r["result"]["feedback"] == "second"
203
+
204
+ def test_revert(self, workspace):
205
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
206
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
207
+ r = _run(workspace, "snap", "revert", "snap-001")
208
+ assert r["result"]["status"] == "reverted"
209
+
210
+ def test_revert_is_terminal(self, workspace):
211
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
212
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
213
+ _run(workspace, "snap", "revert", "snap-001")
214
+ r = _run(workspace, "snap", "revert", "snap-001")
215
+ assert r["error"]["code"] == "STATE_CONFLICT"
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Decision commands
220
+ # ---------------------------------------------------------------------------
221
+
222
+ class TestDecision:
223
+ def test_create(self, workspace):
224
+ r = _run(workspace, "decision", "create", "Rule", "--rationale", "reason")
225
+ assert r["ok"] is True
226
+ assert r["result"]["id"] == "decision-001"
227
+ assert r["result"]["status"] == "active"
228
+
229
+ def test_create_auto_attaches_intents(self, workspace):
230
+ _run(workspace, "intent", "create", "A", "--query", "q")
231
+ r = _run(workspace, "decision", "create", "Rule", "--rationale", "r")
232
+ assert "intent-001" in r["result"]["intent_ids"]
233
+ # Verify bidirectional
234
+ i = _run(workspace, "intent", "show", "intent-001")
235
+ assert "decision-001" in i["result"]["decision_ids"]
236
+
237
+ def test_list(self, workspace):
238
+ _run(workspace, "decision", "create", "R1", "--rationale", "r")
239
+ _run(workspace, "decision", "create", "R2", "--rationale", "r")
240
+ r = _run(workspace, "decision", "list")
241
+ assert len(r["result"]) == 2
242
+
243
+ def test_deprecate(self, workspace):
244
+ _run(workspace, "decision", "create", "R", "--rationale", "r")
245
+ r = _run(workspace, "decision", "deprecate", "decision-001")
246
+ assert r["result"]["status"] == "deprecated"
247
+
248
+ def test_deprecate_is_terminal(self, workspace):
249
+ _run(workspace, "decision", "create", "R", "--rationale", "r")
250
+ _run(workspace, "decision", "deprecate", "decision-001")
251
+ r = _run(workspace, "decision", "deprecate", "decision-001")
252
+ assert r["error"]["code"] == "STATE_CONFLICT"
253
+
254
+ def test_deprecated_not_auto_attached(self, workspace):
255
+ _run(workspace, "decision", "create", "R", "--rationale", "r")
256
+ _run(workspace, "decision", "deprecate", "decision-001")
257
+ r = _run(workspace, "intent", "create", "New goal", "--query", "q")
258
+ assert "decision-001" not in r["result"]["decision_ids"]
259
+
260
+ def test_attach(self, workspace):
261
+ _run(workspace, "intent", "create", "A", "--query", "q")
262
+ _run(workspace, "intent", "create", "B", "--query", "q")
263
+ _run(workspace, "decision", "create", "R", "--rationale", "r")
264
+ # decision-001 auto-attached to both. Manually attach to verify idempotency.
265
+ r = _run(workspace, "decision", "attach", "decision-001", "--intent", "intent-001")
266
+ assert r["ok"] is True
267
+
268
+ def test_attach_not_found(self, workspace):
269
+ _run(workspace, "decision", "create", "R", "--rationale", "r")
270
+ r = _run(workspace, "decision", "attach", "decision-001", "--intent", "intent-999")
271
+ assert r["error"]["code"] == "OBJECT_NOT_FOUND"
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # Inspect
276
+ # ---------------------------------------------------------------------------
277
+
278
+ class TestInspect:
279
+ def test_full_graph(self, workspace):
280
+ _run(workspace, "intent", "create", "Active", "--query", "q")
281
+ _run(workspace, "intent", "create", "Will suspend", "--query", "q")
282
+ _run(workspace, "intent", "suspend", "intent-002")
283
+ _run(workspace, "decision", "create", "Rule", "--rationale", "r")
284
+ _run(workspace, "snap", "create", "S1", "--intent", "intent-001",
285
+ "--summary", "did something")
286
+
287
+ r = _run(workspace, "inspect")
288
+ assert r["ok"] is True
289
+ assert len(r["active_intents"]) == 1
290
+ assert r["active_intents"][0]["id"] == "intent-001"
291
+ assert r["active_intents"][0]["latest_snap_id"] == "snap-001"
292
+ assert len(r["suspend_intents"]) == 1
293
+ assert r["suspend_intents"][0]["id"] == "intent-002"
294
+ assert len(r["active_decisions"]) == 1
295
+ assert len(r["recent_snaps"]) == 1
296
+
297
+ def test_orphan_snap_warning(self, workspace):
298
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
299
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
300
+ # Delete intent file to create orphan
301
+ intent_file = workspace / ".intent" / "intents" / "intent-001.json"
302
+ intent_file.unlink()
303
+ r = _run(workspace, "inspect")
304
+ assert any("Orphan" in w for w in r["warnings"])