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.
- {intent_cli_python-1.0.0/src/intent_cli_python.egg-info → intent_cli_python-1.1.0}/PKG-INFO +22 -5
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/README.md +21 -4
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/pyproject.toml +1 -1
- intent_cli_python-1.1.0/src/intent_cli/__init__.py +8 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/cli.py +36 -5
- intent_cli_python-1.1.0/src/intent_cli/store.py +228 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0/src/intent_cli_python.egg-info}/PKG-INFO +22 -5
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/tests/test_cli.py +67 -3
- intent_cli_python-1.0.0/src/intent_cli/__init__.py +0 -1
- intent_cli_python-1.0.0/src/intent_cli/store.py +0 -90
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/LICENSE +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/setup.cfg +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/__main__.py +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli/output.py +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/SOURCES.txt +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/dependency_links.txt +0 -0
- {intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/entry_points.txt +0 -0
- {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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
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"
|
|
@@ -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":
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
###
|
|
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
|
|
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
|
-
["
|
|
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"
|
|
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())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{intent_cli_python-1.0.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/top_level.txt
RENAMED
|
File without changes
|