intent-cli-python 1.0.0__tar.gz → 1.1.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 (18) hide show
  1. {intent_cli_python-1.0.0/src/intent_cli_python.egg-info → intent_cli_python-1.1.0}/PKG-INFO +22 -5
  2. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/README.md +21 -4
  3. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/pyproject.toml +1 -1
  4. intent_cli_python-1.1.0/src/intent_cli/__init__.py +8 -0
  5. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/cli.py +36 -5
  6. intent_cli_python-1.1.0/src/intent_cli/store.py +228 -0
  7. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0/src/intent_cli_python.egg-info}/PKG-INFO +22 -5
  8. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/tests/test_cli.py +67 -3
  9. intent_cli_python-1.0.0/src/intent_cli/__init__.py +0 -1
  10. intent_cli_python-1.0.0/src/intent_cli/store.py +0 -90
  11. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/LICENSE +0 -0
  12. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/setup.cfg +0 -0
  13. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/__main__.py +0 -0
  14. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/output.py +0 -0
  15. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/SOURCES.txt +0 -0
  16. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/dependency_links.txt +0 -0
  17. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/entry_points.txt +0 -0
  18. {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 1.0.0
3
+ Version: 1.1.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
@@ -30,6 +30,16 @@ Semantic history for agent-driven development. Records **what you did** and **wh
30
30
 
31
31
  Intent CLI gives AI agents a structured way to track goals, interactions, and decisions across sessions. Instead of losing context when a conversation ends, agents persist their understanding into three simple objects stored alongside your code.
32
32
 
33
+ ## Why
34
+
35
+ Git records how code changes. But it doesn't record **why you're on this path**, what you decided along the way, or where you left off.
36
+
37
+ Today that context lives in chat logs, PR threads, and your head. It works — until the session ends, the agent forgets, or a teammate picks up your work blind.
38
+
39
+ Intent treats these as a missing layer: **semantic history**. Not more docs, not better commit messages — a small set of formal objects that capture goals, interactions, and decisions so they survive context loss.
40
+
41
+ > The shift is simple: development is moving from *writing code* to *guiding agents and distilling decisions*. The history layer should reflect that.
42
+
33
43
  ## Three objects, one graph
34
44
 
35
45
  | Object | What it captures |
@@ -62,12 +72,18 @@ pip install intent-cli-python
62
72
 
63
73
  Requires Python 3.9+ and Git.
64
74
 
65
- ### Add the Claude Code skill
75
+ ### Install the skills.sh skill
66
76
 
67
77
  ```bash
68
- npx skills add dozybot001/Intent
78
+ npx skills add dozybot001/Intent -g
69
79
  ```
70
80
 
81
+ This installs the `intent-cli` skill into your global skills library for supported agents such as Codex and Claude Code.
82
+
83
+ > **Tip:** `itt` is a new tool — current models have never seen it in training data. Your agent may forget to call it mid-conversation. A short nudge like *"use itt to record this"* is usually enough to bring it back on track.
84
+ >
85
+ > This isn't busywork — every record is a **semantic asset**. An upcoming platform, **IntHub**, will turn these assets into searchable, shareable project intelligence.
86
+
71
87
  ## Quick start
72
88
 
73
89
  ```bash
@@ -101,13 +117,14 @@ itt inspect
101
117
  | `itt version` | Print version |
102
118
  | `itt init` | Initialize `.intent/` in current git repo |
103
119
  | `itt inspect` | Show the live object graph snapshot |
120
+ | `itt doctor` | Validate the object graph for broken references and invalid states |
104
121
 
105
122
  ### Intent
106
123
 
107
124
  | Command | Description |
108
125
  |---|---|
109
126
  | `itt intent create TITLE --query Q` | Create a new intent |
110
- | `itt intent list [--status S]` | List intents |
127
+ | `itt intent list [--status S] [--decision ID]` | List intents |
111
128
  | `itt intent show ID` | Show intent details |
112
129
  | `itt intent activate ID` | Resume a suspended intent |
113
130
  | `itt intent suspend ID` | Suspend an active intent |
@@ -128,7 +145,7 @@ itt inspect
128
145
  | Command | Description |
129
146
  |---|---|
130
147
  | `itt decision create TITLE --rationale R` | Create a long-lived decision |
131
- | `itt decision list [--status S]` | List decisions |
148
+ | `itt decision list [--status S] [--intent ID]` | List decisions |
132
149
  | `itt decision show ID` | Show decision details |
133
150
  | `itt decision deprecate ID` | Deprecate a decision |
134
151
  | `itt decision attach ID --intent ID` | Manually link a decision to an intent |
@@ -6,6 +6,16 @@ Semantic history for agent-driven development. Records **what you did** and **wh
6
6
 
7
7
  Intent CLI gives AI agents a structured way to track goals, interactions, and decisions across sessions. Instead of losing context when a conversation ends, agents persist their understanding into three simple objects stored alongside your code.
8
8
 
9
+ ## Why
10
+
11
+ Git records how code changes. But it doesn't record **why you're on this path**, what you decided along the way, or where you left off.
12
+
13
+ Today that context lives in chat logs, PR threads, and your head. It works — until the session ends, the agent forgets, or a teammate picks up your work blind.
14
+
15
+ Intent treats these as a missing layer: **semantic history**. Not more docs, not better commit messages — a small set of formal objects that capture goals, interactions, and decisions so they survive context loss.
16
+
17
+ > The shift is simple: development is moving from *writing code* to *guiding agents and distilling decisions*. The history layer should reflect that.
18
+
9
19
  ## Three objects, one graph
10
20
 
11
21
  | Object | What it captures |
@@ -38,12 +48,18 @@ pip install intent-cli-python
38
48
 
39
49
  Requires Python 3.9+ and Git.
40
50
 
41
- ### Add the Claude Code skill
51
+ ### Install the skills.sh skill
42
52
 
43
53
  ```bash
44
- npx skills add dozybot001/Intent
54
+ npx skills add dozybot001/Intent -g
45
55
  ```
46
56
 
57
+ This installs the `intent-cli` skill into your global skills library for supported agents such as Codex and Claude Code.
58
+
59
+ > **Tip:** `itt` is a new tool — current models have never seen it in training data. Your agent may forget to call it mid-conversation. A short nudge like *"use itt to record this"* is usually enough to bring it back on track.
60
+ >
61
+ > This isn't busywork — every record is a **semantic asset**. An upcoming platform, **IntHub**, will turn these assets into searchable, shareable project intelligence.
62
+
47
63
  ## Quick start
48
64
 
49
65
  ```bash
@@ -77,13 +93,14 @@ itt inspect
77
93
  | `itt version` | Print version |
78
94
  | `itt init` | Initialize `.intent/` in current git repo |
79
95
  | `itt inspect` | Show the live object graph snapshot |
96
+ | `itt doctor` | Validate the object graph for broken references and invalid states |
80
97
 
81
98
  ### Intent
82
99
 
83
100
  | Command | Description |
84
101
  |---|---|
85
102
  | `itt intent create TITLE --query Q` | Create a new intent |
86
- | `itt intent list [--status S]` | List intents |
103
+ | `itt intent list [--status S] [--decision ID]` | List intents |
87
104
  | `itt intent show ID` | Show intent details |
88
105
  | `itt intent activate ID` | Resume a suspended intent |
89
106
  | `itt intent suspend ID` | Suspend an active intent |
@@ -104,7 +121,7 @@ itt inspect
104
121
  | Command | Description |
105
122
  |---|---|
106
123
  | `itt decision create TITLE --rationale R` | Create a long-lived decision |
107
- | `itt decision list [--status S]` | List decisions |
124
+ | `itt decision list [--status S] [--intent ID]` | List decisions |
108
125
  | `itt decision show ID` | Show decision details |
109
126
  | `itt decision deprecate ID` | Deprecate a decision |
110
127
  | `itt decision attach ID --intent ID` | Manually link a decision to an intent |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intent-cli-python"
7
- version = "1.0.0"
7
+ version = "1.1.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"
@@ -0,0 +1,8 @@
1
+ """Intent CLI — semantic history for agent-driven development."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("intent-cli-python")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -5,14 +5,14 @@ import json
5
5
  import sys
6
6
  from datetime import datetime, timezone
7
7
 
8
+ from intent_cli import __version__
8
9
  from intent_cli.output import success, error
9
10
  from intent_cli.store import (
10
11
  git_root, ensure_init, init_workspace,
11
12
  next_id, read_object, write_object, list_objects, read_config,
13
+ validate_graph, VALID_STATUSES,
12
14
  )
13
15
 
14
- VERSION = "1.0.0"
15
-
16
16
 
17
17
  def _now():
18
18
  return datetime.now(timezone.utc).isoformat()
@@ -30,12 +30,25 @@ def _require_init():
30
30
  suggested_fix="itt init")
31
31
 
32
32
 
33
+ def _validate_status_filter(object_type, status):
34
+ """Validate a --status filter against the object's state machine."""
35
+ if status is None:
36
+ return
37
+ allowed = sorted(VALID_STATUSES[object_type])
38
+ if status not in allowed:
39
+ error(
40
+ "INVALID_INPUT",
41
+ f"Invalid status '{status}' for {object_type}. Allowed values: {', '.join(allowed)}.",
42
+ suggested_fix=f"Use one of: {', '.join(allowed)}",
43
+ )
44
+
45
+
33
46
  # ---------------------------------------------------------------------------
34
47
  # Global commands
35
48
  # ---------------------------------------------------------------------------
36
49
 
37
50
  def cmd_version(_args):
38
- success("version", {"version": VERSION})
51
+ success("version", {"version": __version__})
39
52
 
40
53
 
41
54
  def cmd_init(_args):
@@ -107,6 +120,11 @@ def cmd_inspect(_args):
107
120
  }, indent=2, ensure_ascii=False))
108
121
 
109
122
 
123
+ def cmd_doctor(_args):
124
+ base = _require_init()
125
+ success("doctor", validate_graph(base))
126
+
127
+
110
128
  # ---------------------------------------------------------------------------
111
129
  # Intent commands
112
130
  # ---------------------------------------------------------------------------
@@ -145,7 +163,11 @@ def cmd_intent_create(args):
145
163
 
146
164
  def cmd_intent_list(args):
147
165
  base = _require_init()
148
- success("intent.list", list_objects(base, "intent", status=args.status))
166
+ _validate_status_filter("intent", args.status)
167
+ objects = list_objects(base, "intent", status=args.status)
168
+ if args.decision:
169
+ objects = [obj for obj in objects if args.decision in obj.get("decision_ids", [])]
170
+ success("intent.list", objects)
149
171
 
150
172
 
151
173
  def cmd_intent_show(args):
@@ -249,6 +271,7 @@ def cmd_snap_create(args):
249
271
 
250
272
  def cmd_snap_list(args):
251
273
  base = _require_init()
274
+ _validate_status_filter("snap", args.status)
252
275
  objects = list_objects(base, "snap", status=args.status)
253
276
  if args.intent:
254
277
  objects = [s for s in objects if s.get("intent_id") == args.intent]
@@ -324,7 +347,11 @@ def cmd_decision_create(args):
324
347
 
325
348
  def cmd_decision_list(args):
326
349
  base = _require_init()
327
- success("decision.list", list_objects(base, "decision", status=args.status))
350
+ _validate_status_filter("decision", args.status)
351
+ objects = list_objects(base, "decision", status=args.status)
352
+ if args.intent:
353
+ objects = [obj for obj in objects if args.intent in obj.get("intent_ids", [])]
354
+ success("decision.list", objects)
328
355
 
329
356
 
330
357
  def cmd_decision_show(args):
@@ -384,6 +411,7 @@ def main():
384
411
  sub.add_parser("version")
385
412
  sub.add_parser("init")
386
413
  sub.add_parser("inspect")
414
+ sub.add_parser("doctor")
387
415
 
388
416
  # --- intent ---
389
417
  p_intent = sub.add_parser("intent")
@@ -396,6 +424,7 @@ def main():
396
424
 
397
425
  p = s_intent.add_parser("list")
398
426
  p.add_argument("--status", default=None)
427
+ p.add_argument("--decision", default=None)
399
428
 
400
429
  p = s_intent.add_parser("show")
401
430
  p.add_argument("id")
@@ -445,6 +474,7 @@ def main():
445
474
 
446
475
  p = s_decision.add_parser("list")
447
476
  p.add_argument("--status", default=None)
477
+ p.add_argument("--intent", default=None)
448
478
 
449
479
  p = s_decision.add_parser("show")
450
480
  p.add_argument("id")
@@ -467,6 +497,7 @@ def main():
467
497
  "version": cmd_version,
468
498
  "init": cmd_init,
469
499
  "inspect": cmd_inspect,
500
+ "doctor": cmd_doctor,
470
501
  }
471
502
  if args.command in dispatch_global:
472
503
  dispatch_global[args.command](args)
@@ -0,0 +1,228 @@
1
+ """Storage layer — .intent/ directory I/O and ID generation."""
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ INTENT_DIR = ".intent"
8
+ SUBDIRS = {"intent": "intents", "snap": "snaps", "decision": "decisions"}
9
+ VALID_STATUSES = {
10
+ "intent": {"active", "suspend", "done"},
11
+ "snap": {"active", "reverted"},
12
+ "decision": {"active", "deprecated"},
13
+ }
14
+
15
+
16
+ def git_root():
17
+ """Return git repo root as Path, or None."""
18
+ try:
19
+ out = subprocess.run(
20
+ ["git", "rev-parse", "--show-toplevel"],
21
+ capture_output=True, text=True, check=True,
22
+ )
23
+ return Path(out.stdout.strip())
24
+ except (subprocess.CalledProcessError, FileNotFoundError):
25
+ return None
26
+
27
+
28
+ def intent_dir():
29
+ """Return Path to .intent/, or None if not in a git repo."""
30
+ root = git_root()
31
+ return root / INTENT_DIR if root else None
32
+
33
+
34
+ def ensure_init():
35
+ """Return .intent/ Path if initialized, else None."""
36
+ d = intent_dir()
37
+ return d if d and d.is_dir() else None
38
+
39
+
40
+ def init_workspace():
41
+ """Create .intent/ structure. Returns (path, error_code)."""
42
+ root = git_root()
43
+ if root is None:
44
+ return None, "GIT_STATE_INVALID"
45
+ d = root / INTENT_DIR
46
+ if d.is_dir():
47
+ return None, "ALREADY_EXISTS"
48
+ d.mkdir()
49
+ for sub in SUBDIRS.values():
50
+ (d / sub).mkdir()
51
+ (d / "config.json").write_text(json.dumps({"schema_version": "1.0"}, indent=2))
52
+ return d, None
53
+
54
+
55
+ def next_id(base, object_type):
56
+ """Generate next zero-padded ID for a given object type."""
57
+ subdir = base / SUBDIRS[object_type]
58
+ max_num = 0
59
+ for f in subdir.glob(f"{object_type}-*.json"):
60
+ try:
61
+ num = int(f.stem.split("-", 1)[1])
62
+ max_num = max(max_num, num)
63
+ except (ValueError, IndexError):
64
+ continue
65
+ return f"{object_type}-{max_num + 1:03d}"
66
+
67
+
68
+ def read_object(base, object_type, obj_id):
69
+ """Read object JSON by ID. Returns dict or None."""
70
+ path = base / SUBDIRS[object_type] / f"{obj_id}.json"
71
+ if not path.is_file():
72
+ return None
73
+ return json.loads(path.read_text())
74
+
75
+
76
+ def write_object(base, object_type, obj_id, data):
77
+ """Write object dict to JSON file."""
78
+ path = base / SUBDIRS[object_type] / f"{obj_id}.json"
79
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
80
+
81
+
82
+ def list_objects(base, object_type, status=None):
83
+ """List all objects of a type, optionally filtered by status."""
84
+ subdir = base / SUBDIRS[object_type]
85
+ result = []
86
+ for f in sorted(subdir.glob(f"{object_type}-*.json")):
87
+ obj = json.loads(f.read_text())
88
+ if status is None or obj.get("status") == status:
89
+ result.append(obj)
90
+ return result
91
+
92
+
93
+ def read_config(base):
94
+ """Read config.json. Returns dict."""
95
+ return json.loads((base / "config.json").read_text())
96
+
97
+
98
+ def validate_graph(base):
99
+ """Validate the object graph and return a structured report."""
100
+ config = read_config(base)
101
+ intents = {obj["id"]: obj for obj in list_objects(base, "intent")}
102
+ snaps = {obj["id"]: obj for obj in list_objects(base, "snap")}
103
+ decisions = {obj["id"]: obj for obj in list_objects(base, "decision")}
104
+ issues = []
105
+
106
+ if config.get("schema_version") != "1.0":
107
+ issues.append({
108
+ "code": "SCHEMA_VERSION_MISMATCH",
109
+ "object": "config",
110
+ "id": "config.json",
111
+ "message": f"Unsupported schema_version '{config.get('schema_version')}'. Expected '1.0'.",
112
+ })
113
+
114
+ def add_issue(code, object_type, obj_id, message):
115
+ issues.append({
116
+ "code": code,
117
+ "object": object_type,
118
+ "id": obj_id,
119
+ "message": message,
120
+ })
121
+
122
+ for object_type, objects in (
123
+ ("intent", intents),
124
+ ("snap", snaps),
125
+ ("decision", decisions),
126
+ ):
127
+ for obj_id, obj in objects.items():
128
+ if obj.get("object") != object_type:
129
+ add_issue(
130
+ "OBJECT_TYPE_MISMATCH",
131
+ object_type,
132
+ obj_id,
133
+ f"Stored object type is '{obj.get('object')}', expected '{object_type}'.",
134
+ )
135
+ status = obj.get("status")
136
+ if status not in VALID_STATUSES[object_type]:
137
+ add_issue(
138
+ "INVALID_STATUS",
139
+ object_type,
140
+ obj_id,
141
+ f"Invalid status '{status}' for {object_type}.",
142
+ )
143
+
144
+ for intent_id, intent in intents.items():
145
+ for snap_id in intent.get("snap_ids", []):
146
+ snap = snaps.get(snap_id)
147
+ if snap is None:
148
+ add_issue(
149
+ "MISSING_REFERENCE",
150
+ "intent",
151
+ intent_id,
152
+ f"References missing snap {snap_id} in snap_ids.",
153
+ )
154
+ continue
155
+ if snap.get("intent_id") != intent_id:
156
+ add_issue(
157
+ "BROKEN_LINK",
158
+ "intent",
159
+ intent_id,
160
+ f"Snap {snap_id} points to intent {snap.get('intent_id')}, not {intent_id}.",
161
+ )
162
+ for decision_id in intent.get("decision_ids", []):
163
+ decision = decisions.get(decision_id)
164
+ if decision is None:
165
+ add_issue(
166
+ "MISSING_REFERENCE",
167
+ "intent",
168
+ intent_id,
169
+ f"References missing decision {decision_id} in decision_ids.",
170
+ )
171
+ continue
172
+ if intent_id not in decision.get("intent_ids", []):
173
+ add_issue(
174
+ "BROKEN_LINK",
175
+ "intent",
176
+ intent_id,
177
+ f"Decision {decision_id} does not link back to this intent.",
178
+ )
179
+
180
+ for snap_id, snap in snaps.items():
181
+ intent_id = snap.get("intent_id")
182
+ intent = intents.get(intent_id)
183
+ if intent is None:
184
+ add_issue(
185
+ "MISSING_REFERENCE",
186
+ "snap",
187
+ snap_id,
188
+ f"Points to missing intent {intent_id}.",
189
+ )
190
+ continue
191
+ if snap_id not in intent.get("snap_ids", []):
192
+ add_issue(
193
+ "BROKEN_LINK",
194
+ "snap",
195
+ snap_id,
196
+ f"Intent {intent_id} does not include this snap in snap_ids.",
197
+ )
198
+
199
+ for decision_id, decision in decisions.items():
200
+ for intent_id in decision.get("intent_ids", []):
201
+ intent = intents.get(intent_id)
202
+ if intent is None:
203
+ add_issue(
204
+ "MISSING_REFERENCE",
205
+ "decision",
206
+ decision_id,
207
+ f"References missing intent {intent_id} in intent_ids.",
208
+ )
209
+ continue
210
+ if decision_id not in intent.get("decision_ids", []):
211
+ add_issue(
212
+ "BROKEN_LINK",
213
+ "decision",
214
+ decision_id,
215
+ f"Intent {intent_id} does not link back to this decision.",
216
+ )
217
+
218
+ return {
219
+ "healthy": not issues,
220
+ "issue_count": len(issues),
221
+ "summary": {
222
+ "schema_version": config.get("schema_version", "1.0"),
223
+ "intent_count": len(intents),
224
+ "snap_count": len(snaps),
225
+ "decision_count": len(decisions),
226
+ },
227
+ "issues": issues,
228
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intent-cli-python
3
- Version: 1.0.0
3
+ Version: 1.1.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
@@ -30,6 +30,16 @@ Semantic history for agent-driven development. Records **what you did** and **wh
30
30
 
31
31
  Intent CLI gives AI agents a structured way to track goals, interactions, and decisions across sessions. Instead of losing context when a conversation ends, agents persist their understanding into three simple objects stored alongside your code.
32
32
 
33
+ ## Why
34
+
35
+ Git records how code changes. But it doesn't record **why you're on this path**, what you decided along the way, or where you left off.
36
+
37
+ Today that context lives in chat logs, PR threads, and your head. It works — until the session ends, the agent forgets, or a teammate picks up your work blind.
38
+
39
+ Intent treats these as a missing layer: **semantic history**. Not more docs, not better commit messages — a small set of formal objects that capture goals, interactions, and decisions so they survive context loss.
40
+
41
+ > The shift is simple: development is moving from *writing code* to *guiding agents and distilling decisions*. The history layer should reflect that.
42
+
33
43
  ## Three objects, one graph
34
44
 
35
45
  | Object | What it captures |
@@ -62,12 +72,18 @@ pip install intent-cli-python
62
72
 
63
73
  Requires Python 3.9+ and Git.
64
74
 
65
- ### Add the Claude Code skill
75
+ ### Install the skills.sh skill
66
76
 
67
77
  ```bash
68
- npx skills add dozybot001/Intent
78
+ npx skills add dozybot001/Intent -g
69
79
  ```
70
80
 
81
+ This installs the `intent-cli` skill into your global skills library for supported agents such as Codex and Claude Code.
82
+
83
+ > **Tip:** `itt` is a new tool — current models have never seen it in training data. Your agent may forget to call it mid-conversation. A short nudge like *"use itt to record this"* is usually enough to bring it back on track.
84
+ >
85
+ > This isn't busywork — every record is a **semantic asset**. An upcoming platform, **IntHub**, will turn these assets into searchable, shareable project intelligence.
86
+
71
87
  ## Quick start
72
88
 
73
89
  ```bash
@@ -101,13 +117,14 @@ itt inspect
101
117
  | `itt version` | Print version |
102
118
  | `itt init` | Initialize `.intent/` in current git repo |
103
119
  | `itt inspect` | Show the live object graph snapshot |
120
+ | `itt doctor` | Validate the object graph for broken references and invalid states |
104
121
 
105
122
  ### Intent
106
123
 
107
124
  | Command | Description |
108
125
  |---|---|
109
126
  | `itt intent create TITLE --query Q` | Create a new intent |
110
- | `itt intent list [--status S]` | List intents |
127
+ | `itt intent list [--status S] [--decision ID]` | List intents |
111
128
  | `itt intent show ID` | Show intent details |
112
129
  | `itt intent activate ID` | Resume a suspended intent |
113
130
  | `itt intent suspend ID` | Suspend an active intent |
@@ -128,7 +145,7 @@ itt inspect
128
145
  | Command | Description |
129
146
  |---|---|
130
147
  | `itt decision create TITLE --rationale R` | Create a long-lived decision |
131
- | `itt decision list [--status S]` | List decisions |
148
+ | `itt decision list [--status S] [--intent ID]` | List decisions |
132
149
  | `itt decision show ID` | Show decision details |
133
150
  | `itt decision deprecate ID` | Deprecate a decision |
134
151
  | `itt decision attach ID --intent ID` | Manually link a decision to an intent |
@@ -1,9 +1,11 @@
1
- """Tests for Intent CLI — covers all 19 commands, state machines, and error codes."""
1
+ """Tests for Intent CLI — covers all 20 commands, state machines, and error codes."""
2
2
 
3
3
  import json
4
4
  import os
5
5
  import subprocess
6
+ import sys
6
7
  import tempfile
8
+ from importlib import metadata
7
9
  from pathlib import Path
8
10
 
9
11
  import pytest
@@ -25,7 +27,7 @@ def workspace(tmp_path):
25
27
  def _run(cwd, *args):
26
28
  """Run itt command and return parsed JSON."""
27
29
  r = subprocess.run(
28
- ["itt", *args],
30
+ [sys.executable, "-m", "intent_cli", *args],
29
31
  cwd=cwd, capture_output=True, text=True,
30
32
  )
31
33
  return json.loads(r.stdout)
@@ -39,7 +41,7 @@ class TestGlobal:
39
41
  def test_version(self, workspace):
40
42
  r = _run(workspace, "version")
41
43
  assert r["ok"] is True
42
- assert "version" in r["result"]
44
+ assert r["result"]["version"] == metadata.version("intent-cli-python")
43
45
 
44
46
  def test_init_already_exists(self, workspace):
45
47
  r = _run(workspace, "init")
@@ -68,6 +70,12 @@ class TestGlobal:
68
70
  assert r["ok"] is False
69
71
  assert r["error"]["code"] == "NOT_INITIALIZED"
70
72
 
73
+ def test_doctor_healthy(self, workspace):
74
+ r = _run(workspace, "doctor")
75
+ assert r["ok"] is True
76
+ assert r["result"]["healthy"] is True
77
+ assert r["result"]["issue_count"] == 0
78
+
71
79
 
72
80
  # ---------------------------------------------------------------------------
73
81
  # Intent commands
@@ -101,6 +109,20 @@ class TestIntent:
101
109
  assert len(r["result"]) == 1
102
110
  assert r["result"][0]["id"] == "intent-002"
103
111
 
112
+ def test_list_filter_decision(self, workspace):
113
+ _run(workspace, "intent", "create", "A", "--query", "q")
114
+ _run(workspace, "intent", "create", "B", "--query", "q")
115
+ _run(workspace, "decision", "create", "Rule", "--rationale", "r")
116
+ _run(workspace, "decision", "deprecate", "decision-001")
117
+ _run(workspace, "decision", "create", "Rule 2", "--rationale", "r")
118
+ r = _run(workspace, "intent", "list", "--decision", "decision-001")
119
+ assert len(r["result"]) == 2
120
+
121
+ def test_list_invalid_status(self, workspace):
122
+ r = _run(workspace, "intent", "list", "--status", "paused")
123
+ assert r["ok"] is False
124
+ assert r["error"]["code"] == "INVALID_INPUT"
125
+
104
126
  def test_show(self, workspace):
105
127
  _run(workspace, "intent", "create", "A", "--query", "q")
106
128
  r = _run(workspace, "intent", "show", "intent-001")
@@ -188,6 +210,11 @@ class TestSnap:
188
210
  assert len(r["result"]) == 1
189
211
  assert r["result"][0]["id"] == "snap-002"
190
212
 
213
+ def test_list_invalid_status(self, workspace):
214
+ r = _run(workspace, "snap", "list", "--status", "done")
215
+ assert r["ok"] is False
216
+ assert r["error"]["code"] == "INVALID_INPUT"
217
+
191
218
  def test_feedback(self, workspace):
192
219
  _run(workspace, "intent", "create", "Goal", "--query", "q")
193
220
  _run(workspace, "snap", "create", "S", "--intent", "intent-001")
@@ -240,6 +267,22 @@ class TestDecision:
240
267
  r = _run(workspace, "decision", "list")
241
268
  assert len(r["result"]) == 2
242
269
 
270
+ def test_list_filter_intent(self, workspace):
271
+ _run(workspace, "intent", "create", "A", "--query", "q")
272
+ _run(workspace, "intent", "create", "B", "--query", "q")
273
+ _run(workspace, "decision", "create", "Rule A", "--rationale", "r")
274
+ _run(workspace, "decision", "deprecate", "decision-001")
275
+ _run(workspace, "intent", "done", "intent-002")
276
+ _run(workspace, "decision", "create", "Rule B", "--rationale", "r")
277
+ r = _run(workspace, "decision", "list", "--intent", "intent-002")
278
+ assert len(r["result"]) == 1
279
+ assert r["result"][0]["id"] == "decision-001"
280
+
281
+ def test_list_invalid_status(self, workspace):
282
+ r = _run(workspace, "decision", "list", "--status", "activeish")
283
+ assert r["ok"] is False
284
+ assert r["error"]["code"] == "INVALID_INPUT"
285
+
243
286
  def test_deprecate(self, workspace):
244
287
  _run(workspace, "decision", "create", "R", "--rationale", "r")
245
288
  r = _run(workspace, "decision", "deprecate", "decision-001")
@@ -302,3 +345,24 @@ class TestInspect:
302
345
  intent_file.unlink()
303
346
  r = _run(workspace, "inspect")
304
347
  assert any("Orphan" in w for w in r["warnings"])
348
+
349
+ def test_doctor_reports_broken_links(self, workspace):
350
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
351
+ _run(workspace, "snap", "create", "S", "--intent", "intent-001")
352
+ snap_file = workspace / ".intent" / "snaps" / "snap-001.json"
353
+ data = json.loads(snap_file.read_text())
354
+ data["intent_id"] = "intent-999"
355
+ snap_file.write_text(json.dumps(data, indent=2))
356
+ r = _run(workspace, "doctor")
357
+ assert r["result"]["healthy"] is False
358
+ assert any(issue["code"] == "MISSING_REFERENCE" for issue in r["result"]["issues"])
359
+
360
+ def test_doctor_reports_invalid_status(self, workspace):
361
+ _run(workspace, "intent", "create", "Goal", "--query", "q")
362
+ intent_file = workspace / ".intent" / "intents" / "intent-001.json"
363
+ data = json.loads(intent_file.read_text())
364
+ data["status"] = "paused"
365
+ intent_file.write_text(json.dumps(data, indent=2))
366
+ r = _run(workspace, "doctor")
367
+ assert r["result"]["healthy"] is False
368
+ assert any(issue["code"] == "INVALID_STATUS" for issue in r["result"]["issues"])
@@ -1 +0,0 @@
1
- """Intent CLI — semantic history for agent-driven development."""
@@ -1,90 +0,0 @@
1
- """Storage layer — .intent/ directory I/O and ID generation."""
2
-
3
- import json
4
- import subprocess
5
- from pathlib import Path
6
-
7
- INTENT_DIR = ".intent"
8
- SUBDIRS = {"intent": "intents", "snap": "snaps", "decision": "decisions"}
9
-
10
-
11
- def git_root():
12
- """Return git repo root as Path, or None."""
13
- try:
14
- out = subprocess.run(
15
- ["git", "rev-parse", "--show-toplevel"],
16
- capture_output=True, text=True, check=True,
17
- )
18
- return Path(out.stdout.strip())
19
- except (subprocess.CalledProcessError, FileNotFoundError):
20
- return None
21
-
22
-
23
- def intent_dir():
24
- """Return Path to .intent/, or None if not in a git repo."""
25
- root = git_root()
26
- return root / INTENT_DIR if root else None
27
-
28
-
29
- def ensure_init():
30
- """Return .intent/ Path if initialized, else None."""
31
- d = intent_dir()
32
- return d if d and d.is_dir() else None
33
-
34
-
35
- def init_workspace():
36
- """Create .intent/ structure. Returns (path, error_code)."""
37
- root = git_root()
38
- if root is None:
39
- return None, "GIT_STATE_INVALID"
40
- d = root / INTENT_DIR
41
- if d.is_dir():
42
- return None, "ALREADY_EXISTS"
43
- d.mkdir()
44
- for sub in SUBDIRS.values():
45
- (d / sub).mkdir()
46
- (d / "config.json").write_text(json.dumps({"schema_version": "1.0"}, indent=2))
47
- return d, None
48
-
49
-
50
- def next_id(base, object_type):
51
- """Generate next zero-padded ID for a given object type."""
52
- subdir = base / SUBDIRS[object_type]
53
- max_num = 0
54
- for f in subdir.glob(f"{object_type}-*.json"):
55
- try:
56
- num = int(f.stem.split("-", 1)[1])
57
- max_num = max(max_num, num)
58
- except (ValueError, IndexError):
59
- continue
60
- return f"{object_type}-{max_num + 1:03d}"
61
-
62
-
63
- def read_object(base, object_type, obj_id):
64
- """Read object JSON by ID. Returns dict or None."""
65
- path = base / SUBDIRS[object_type] / f"{obj_id}.json"
66
- if not path.is_file():
67
- return None
68
- return json.loads(path.read_text())
69
-
70
-
71
- def write_object(base, object_type, obj_id, data):
72
- """Write object dict to JSON file."""
73
- path = base / SUBDIRS[object_type] / f"{obj_id}.json"
74
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
75
-
76
-
77
- def list_objects(base, object_type, status=None):
78
- """List all objects of a type, optionally filtered by status."""
79
- subdir = base / SUBDIRS[object_type]
80
- result = []
81
- for f in sorted(subdir.glob(f"{object_type}-*.json")):
82
- obj = json.loads(f.read_text())
83
- if status is None or obj.get("status") == status:
84
- result.append(obj)
85
- return result
86
-
87
-
88
- def read_config(base):
89
- """Read config.json. Returns dict."""
90
- return json.loads((base / "config.json").read_text())