intent-cli-python 0.6.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-0.6.0/src/intent_cli_python.egg-info → intent_cli_python-1.1.0}/PKG-INFO +47 -4
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/README.md +45 -2
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/pyproject.toml +2 -2
- intent_cli_python-1.1.0/src/intent_cli/__init__.py +8 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli/cli.py +41 -6
- intent_cli_python-1.1.0/src/intent_cli/store.py +228 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0/src/intent_cli_python.egg-info}/PKG-INFO +47 -4
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/SOURCES.txt +2 -1
- intent_cli_python-1.1.0/tests/test_cli.py +368 -0
- intent_cli_python-0.6.0/src/intent_cli/__init__.py +0 -1
- intent_cli_python-0.6.0/src/intent_cli/store.py +0 -90
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/LICENSE +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/setup.cfg +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli/__main__.py +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli/output.py +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/dependency_links.txt +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/entry_points.txt +0 -0
- {intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intent-cli-python
|
|
3
|
-
Version:
|
|
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
|
|
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 ::
|
|
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
|
|
@@ -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 |
|
|
@@ -40,14 +50,40 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
|
|
|
40
50
|
|
|
41
51
|
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
52
|
|
|
53
|
+
### How decisions are created
|
|
54
|
+
|
|
55
|
+
Decisions require human involvement. Two paths:
|
|
56
|
+
|
|
57
|
+
- **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
|
|
58
|
+
- **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
|
|
59
|
+
|
|
43
60
|
## Install
|
|
44
61
|
|
|
45
62
|
```bash
|
|
63
|
+
# Clone the repository
|
|
64
|
+
git clone https://github.com/dozybot001/Intent.git
|
|
65
|
+
|
|
66
|
+
# Install the CLI (pipx recommended)
|
|
67
|
+
pipx install intent-cli-python
|
|
68
|
+
|
|
69
|
+
# Or using pip
|
|
46
70
|
pip install intent-cli-python
|
|
47
71
|
```
|
|
48
72
|
|
|
49
73
|
Requires Python 3.9+ and Git.
|
|
50
74
|
|
|
75
|
+
### Install the skills.sh skill
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add dozybot001/Intent -g
|
|
79
|
+
```
|
|
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
|
+
|
|
51
87
|
## Quick start
|
|
52
88
|
|
|
53
89
|
```bash
|
|
@@ -81,13 +117,14 @@ itt inspect
|
|
|
81
117
|
| `itt version` | Print version |
|
|
82
118
|
| `itt init` | Initialize `.intent/` in current git repo |
|
|
83
119
|
| `itt inspect` | Show the live object graph snapshot |
|
|
120
|
+
| `itt doctor` | Validate the object graph for broken references and invalid states |
|
|
84
121
|
|
|
85
122
|
### Intent
|
|
86
123
|
|
|
87
124
|
| Command | Description |
|
|
88
125
|
|---|---|
|
|
89
126
|
| `itt intent create TITLE --query Q` | Create a new intent |
|
|
90
|
-
| `itt intent list [--status S]` | List intents |
|
|
127
|
+
| `itt intent list [--status S] [--decision ID]` | List intents |
|
|
91
128
|
| `itt intent show ID` | Show intent details |
|
|
92
129
|
| `itt intent activate ID` | Resume a suspended intent |
|
|
93
130
|
| `itt intent suspend ID` | Suspend an active intent |
|
|
@@ -108,7 +145,7 @@ itt inspect
|
|
|
108
145
|
| Command | Description |
|
|
109
146
|
|---|---|
|
|
110
147
|
| `itt decision create TITLE --rationale R` | Create a long-lived decision |
|
|
111
|
-
| `itt decision list [--status S]` | List decisions |
|
|
148
|
+
| `itt decision list [--status S] [--intent ID]` | List decisions |
|
|
112
149
|
| `itt decision show ID` | Show decision details |
|
|
113
150
|
| `itt decision deprecate ID` | Deprecate a decision |
|
|
114
151
|
| `itt decision attach ID --intent ID` | Manually link a decision to an intent |
|
|
@@ -136,6 +173,12 @@ All data lives in `.intent/` at your git repo root:
|
|
|
136
173
|
decision-001.json
|
|
137
174
|
```
|
|
138
175
|
|
|
176
|
+
## Docs
|
|
177
|
+
|
|
178
|
+
- [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
|
|
179
|
+
- [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
|
|
180
|
+
- [Roadmap](docs/EN/roadmap.md) — phase plan
|
|
181
|
+
|
|
139
182
|
## License
|
|
140
183
|
|
|
141
184
|
MIT
|
|
@@ -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 |
|
|
@@ -16,14 +26,40 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
|
|
|
16
26
|
|
|
17
27
|
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
28
|
|
|
29
|
+
### How decisions are created
|
|
30
|
+
|
|
31
|
+
Decisions require human involvement. Two paths:
|
|
32
|
+
|
|
33
|
+
- **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
|
|
34
|
+
- **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
|
|
35
|
+
|
|
19
36
|
## Install
|
|
20
37
|
|
|
21
38
|
```bash
|
|
39
|
+
# Clone the repository
|
|
40
|
+
git clone https://github.com/dozybot001/Intent.git
|
|
41
|
+
|
|
42
|
+
# Install the CLI (pipx recommended)
|
|
43
|
+
pipx install intent-cli-python
|
|
44
|
+
|
|
45
|
+
# Or using pip
|
|
22
46
|
pip install intent-cli-python
|
|
23
47
|
```
|
|
24
48
|
|
|
25
49
|
Requires Python 3.9+ and Git.
|
|
26
50
|
|
|
51
|
+
### Install the skills.sh skill
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx skills add dozybot001/Intent -g
|
|
55
|
+
```
|
|
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
|
+
|
|
27
63
|
## Quick start
|
|
28
64
|
|
|
29
65
|
```bash
|
|
@@ -57,13 +93,14 @@ itt inspect
|
|
|
57
93
|
| `itt version` | Print version |
|
|
58
94
|
| `itt init` | Initialize `.intent/` in current git repo |
|
|
59
95
|
| `itt inspect` | Show the live object graph snapshot |
|
|
96
|
+
| `itt doctor` | Validate the object graph for broken references and invalid states |
|
|
60
97
|
|
|
61
98
|
### Intent
|
|
62
99
|
|
|
63
100
|
| Command | Description |
|
|
64
101
|
|---|---|
|
|
65
102
|
| `itt intent create TITLE --query Q` | Create a new intent |
|
|
66
|
-
| `itt intent list [--status S]` | List intents |
|
|
103
|
+
| `itt intent list [--status S] [--decision ID]` | List intents |
|
|
67
104
|
| `itt intent show ID` | Show intent details |
|
|
68
105
|
| `itt intent activate ID` | Resume a suspended intent |
|
|
69
106
|
| `itt intent suspend ID` | Suspend an active intent |
|
|
@@ -84,7 +121,7 @@ itt inspect
|
|
|
84
121
|
| Command | Description |
|
|
85
122
|
|---|---|
|
|
86
123
|
| `itt decision create TITLE --rationale R` | Create a long-lived decision |
|
|
87
|
-
| `itt decision list [--status S]` | List decisions |
|
|
124
|
+
| `itt decision list [--status S] [--intent ID]` | List decisions |
|
|
88
125
|
| `itt decision show ID` | Show decision details |
|
|
89
126
|
| `itt decision deprecate ID` | Deprecate a decision |
|
|
90
127
|
| `itt decision attach ID --intent ID` | Manually link a decision to an intent |
|
|
@@ -112,6 +149,12 @@ All data lives in `.intent/` at your git repo root:
|
|
|
112
149
|
decision-001.json
|
|
113
150
|
```
|
|
114
151
|
|
|
152
|
+
## Docs
|
|
153
|
+
|
|
154
|
+
- [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
|
|
155
|
+
- [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
|
|
156
|
+
- [Roadmap](docs/EN/roadmap.md) — phase plan
|
|
157
|
+
|
|
115
158
|
## License
|
|
116
159
|
|
|
117
160
|
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "intent-cli-python"
|
|
7
|
-
version = "
|
|
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"
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
keywords = ["agent", "git", "semantic-history", "intent", "developer-tools"]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Environment :: Console",
|
|
19
19
|
"Intended Audience :: Developers",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
@@ -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 = "0.6.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):
|
|
@@ -98,7 +111,7 @@ def cmd_inspect(_args):
|
|
|
98
111
|
|
|
99
112
|
print(json.dumps({
|
|
100
113
|
"ok": True,
|
|
101
|
-
"schema_version": config.get("schema_version", "0
|
|
114
|
+
"schema_version": config.get("schema_version", "1.0"),
|
|
102
115
|
"active_intents": active_intents,
|
|
103
116
|
"suspend_intents": suspend_intents,
|
|
104
117
|
"active_decisions": active_decisions,
|
|
@@ -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
|
# ---------------------------------------------------------------------------
|
|
@@ -129,6 +147,7 @@ def cmd_intent_create(args):
|
|
|
129
147
|
"title": args.title,
|
|
130
148
|
"status": "active",
|
|
131
149
|
"source_query": args.query,
|
|
150
|
+
"rationale": args.rationale,
|
|
132
151
|
"decision_ids": decision_ids,
|
|
133
152
|
"snap_ids": [],
|
|
134
153
|
}
|
|
@@ -144,7 +163,11 @@ def cmd_intent_create(args):
|
|
|
144
163
|
|
|
145
164
|
def cmd_intent_list(args):
|
|
146
165
|
base = _require_init()
|
|
147
|
-
|
|
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)
|
|
148
171
|
|
|
149
172
|
|
|
150
173
|
def cmd_intent_show(args):
|
|
@@ -234,6 +257,7 @@ def cmd_snap_create(args):
|
|
|
234
257
|
"status": "active",
|
|
235
258
|
"intent_id": intent_id,
|
|
236
259
|
"query": args.query,
|
|
260
|
+
"rationale": args.rationale,
|
|
237
261
|
"summary": args.summary,
|
|
238
262
|
"feedback": args.feedback,
|
|
239
263
|
}
|
|
@@ -247,6 +271,7 @@ def cmd_snap_create(args):
|
|
|
247
271
|
|
|
248
272
|
def cmd_snap_list(args):
|
|
249
273
|
base = _require_init()
|
|
274
|
+
_validate_status_filter("snap", args.status)
|
|
250
275
|
objects = list_objects(base, "snap", status=args.status)
|
|
251
276
|
if args.intent:
|
|
252
277
|
objects = [s for s in objects if s.get("intent_id") == args.intent]
|
|
@@ -322,7 +347,11 @@ def cmd_decision_create(args):
|
|
|
322
347
|
|
|
323
348
|
def cmd_decision_list(args):
|
|
324
349
|
base = _require_init()
|
|
325
|
-
|
|
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)
|
|
326
355
|
|
|
327
356
|
|
|
328
357
|
def cmd_decision_show(args):
|
|
@@ -382,6 +411,7 @@ def main():
|
|
|
382
411
|
sub.add_parser("version")
|
|
383
412
|
sub.add_parser("init")
|
|
384
413
|
sub.add_parser("inspect")
|
|
414
|
+
sub.add_parser("doctor")
|
|
385
415
|
|
|
386
416
|
# --- intent ---
|
|
387
417
|
p_intent = sub.add_parser("intent")
|
|
@@ -390,9 +420,11 @@ def main():
|
|
|
390
420
|
p = s_intent.add_parser("create")
|
|
391
421
|
p.add_argument("title")
|
|
392
422
|
p.add_argument("--query", default="")
|
|
423
|
+
p.add_argument("--rationale", default="")
|
|
393
424
|
|
|
394
425
|
p = s_intent.add_parser("list")
|
|
395
426
|
p.add_argument("--status", default=None)
|
|
427
|
+
p.add_argument("--decision", default=None)
|
|
396
428
|
|
|
397
429
|
p = s_intent.add_parser("show")
|
|
398
430
|
p.add_argument("id")
|
|
@@ -414,6 +446,7 @@ def main():
|
|
|
414
446
|
p.add_argument("title")
|
|
415
447
|
p.add_argument("--intent", required=True)
|
|
416
448
|
p.add_argument("--query", default="")
|
|
449
|
+
p.add_argument("--rationale", default="")
|
|
417
450
|
p.add_argument("--summary", default="")
|
|
418
451
|
p.add_argument("--feedback", default="")
|
|
419
452
|
|
|
@@ -441,6 +474,7 @@ def main():
|
|
|
441
474
|
|
|
442
475
|
p = s_decision.add_parser("list")
|
|
443
476
|
p.add_argument("--status", default=None)
|
|
477
|
+
p.add_argument("--intent", default=None)
|
|
444
478
|
|
|
445
479
|
p = s_decision.add_parser("show")
|
|
446
480
|
p.add_argument("id")
|
|
@@ -463,6 +497,7 @@ def main():
|
|
|
463
497
|
"version": cmd_version,
|
|
464
498
|
"init": cmd_init,
|
|
465
499
|
"inspect": cmd_inspect,
|
|
500
|
+
"doctor": cmd_doctor,
|
|
466
501
|
}
|
|
467
502
|
if args.command in dispatch_global:
|
|
468
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,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intent-cli-python
|
|
3
|
-
Version:
|
|
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
|
|
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 ::
|
|
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
|
|
@@ -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 |
|
|
@@ -40,14 +50,40 @@ Intent CLI gives AI agents a structured way to track goals, interactions, and de
|
|
|
40
50
|
|
|
41
51
|
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
52
|
|
|
53
|
+
### How decisions are created
|
|
54
|
+
|
|
55
|
+
Decisions require human involvement. Two paths:
|
|
56
|
+
|
|
57
|
+
- **Explicit**: include `decision-[text]` (or `决定-[text]`) in your query and the agent creates it directly. E.g. "decision-all API responses use envelope format"
|
|
58
|
+
- **Agent-proposed**: the agent spots a potential long-term constraint in conversation and asks you to confirm before recording it
|
|
59
|
+
|
|
43
60
|
## Install
|
|
44
61
|
|
|
45
62
|
```bash
|
|
63
|
+
# Clone the repository
|
|
64
|
+
git clone https://github.com/dozybot001/Intent.git
|
|
65
|
+
|
|
66
|
+
# Install the CLI (pipx recommended)
|
|
67
|
+
pipx install intent-cli-python
|
|
68
|
+
|
|
69
|
+
# Or using pip
|
|
46
70
|
pip install intent-cli-python
|
|
47
71
|
```
|
|
48
72
|
|
|
49
73
|
Requires Python 3.9+ and Git.
|
|
50
74
|
|
|
75
|
+
### Install the skills.sh skill
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add dozybot001/Intent -g
|
|
79
|
+
```
|
|
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
|
+
|
|
51
87
|
## Quick start
|
|
52
88
|
|
|
53
89
|
```bash
|
|
@@ -81,13 +117,14 @@ itt inspect
|
|
|
81
117
|
| `itt version` | Print version |
|
|
82
118
|
| `itt init` | Initialize `.intent/` in current git repo |
|
|
83
119
|
| `itt inspect` | Show the live object graph snapshot |
|
|
120
|
+
| `itt doctor` | Validate the object graph for broken references and invalid states |
|
|
84
121
|
|
|
85
122
|
### Intent
|
|
86
123
|
|
|
87
124
|
| Command | Description |
|
|
88
125
|
|---|---|
|
|
89
126
|
| `itt intent create TITLE --query Q` | Create a new intent |
|
|
90
|
-
| `itt intent list [--status S]` | List intents |
|
|
127
|
+
| `itt intent list [--status S] [--decision ID]` | List intents |
|
|
91
128
|
| `itt intent show ID` | Show intent details |
|
|
92
129
|
| `itt intent activate ID` | Resume a suspended intent |
|
|
93
130
|
| `itt intent suspend ID` | Suspend an active intent |
|
|
@@ -108,7 +145,7 @@ itt inspect
|
|
|
108
145
|
| Command | Description |
|
|
109
146
|
|---|---|
|
|
110
147
|
| `itt decision create TITLE --rationale R` | Create a long-lived decision |
|
|
111
|
-
| `itt decision list [--status S]` | List decisions |
|
|
148
|
+
| `itt decision list [--status S] [--intent ID]` | List decisions |
|
|
112
149
|
| `itt decision show ID` | Show decision details |
|
|
113
150
|
| `itt decision deprecate ID` | Deprecate a decision |
|
|
114
151
|
| `itt decision attach ID --intent ID` | Manually link a decision to an intent |
|
|
@@ -136,6 +173,12 @@ All data lives in `.intent/` at your git repo root:
|
|
|
136
173
|
decision-001.json
|
|
137
174
|
```
|
|
138
175
|
|
|
176
|
+
## Docs
|
|
177
|
+
|
|
178
|
+
- [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
|
|
179
|
+
- [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
|
|
180
|
+
- [Roadmap](docs/EN/roadmap.md) — phase plan
|
|
181
|
+
|
|
139
182
|
## License
|
|
140
183
|
|
|
141
184
|
MIT
|
{intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/SOURCES.txt
RENAMED
|
@@ -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,368 @@
|
|
|
1
|
+
"""Tests for Intent CLI — covers all 20 commands, state machines, and error codes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def workspace(tmp_path):
|
|
16
|
+
"""Create a git repo with .intent/ initialized."""
|
|
17
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
18
|
+
subprocess.run(
|
|
19
|
+
["git", "commit", "--allow-empty", "-m", "init"],
|
|
20
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
21
|
+
)
|
|
22
|
+
result = _run(tmp_path, "init")
|
|
23
|
+
assert result["ok"] is True
|
|
24
|
+
return tmp_path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(cwd, *args):
|
|
28
|
+
"""Run itt command and return parsed JSON."""
|
|
29
|
+
r = subprocess.run(
|
|
30
|
+
[sys.executable, "-m", "intent_cli", *args],
|
|
31
|
+
cwd=cwd, capture_output=True, text=True,
|
|
32
|
+
)
|
|
33
|
+
return json.loads(r.stdout)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Global commands
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
class TestGlobal:
|
|
41
|
+
def test_version(self, workspace):
|
|
42
|
+
r = _run(workspace, "version")
|
|
43
|
+
assert r["ok"] is True
|
|
44
|
+
assert r["result"]["version"] == metadata.version("intent-cli-python")
|
|
45
|
+
|
|
46
|
+
def test_init_already_exists(self, workspace):
|
|
47
|
+
r = _run(workspace, "init")
|
|
48
|
+
assert r["ok"] is False
|
|
49
|
+
assert r["error"]["code"] == "ALREADY_EXISTS"
|
|
50
|
+
|
|
51
|
+
def test_init_not_git(self, tmp_path):
|
|
52
|
+
r = _run(tmp_path, "init")
|
|
53
|
+
assert r["ok"] is False
|
|
54
|
+
assert r["error"]["code"] == "GIT_STATE_INVALID"
|
|
55
|
+
|
|
56
|
+
def test_inspect_empty(self, workspace):
|
|
57
|
+
r = _run(workspace, "inspect")
|
|
58
|
+
assert r["ok"] is True
|
|
59
|
+
assert r["active_intents"] == []
|
|
60
|
+
assert r["active_decisions"] == []
|
|
61
|
+
assert r["recent_snaps"] == []
|
|
62
|
+
|
|
63
|
+
def test_not_initialized(self, tmp_path):
|
|
64
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
65
|
+
subprocess.run(
|
|
66
|
+
["git", "commit", "--allow-empty", "-m", "init"],
|
|
67
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
68
|
+
)
|
|
69
|
+
r = _run(tmp_path, "inspect")
|
|
70
|
+
assert r["ok"] is False
|
|
71
|
+
assert r["error"]["code"] == "NOT_INITIALIZED"
|
|
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
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Intent commands
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
class TestIntent:
|
|
85
|
+
def test_create(self, workspace):
|
|
86
|
+
r = _run(workspace, "intent", "create", "Fix bug", "--query", "why crash?")
|
|
87
|
+
assert r["ok"] is True
|
|
88
|
+
assert r["result"]["id"] == "intent-001"
|
|
89
|
+
assert r["result"]["status"] == "active"
|
|
90
|
+
assert r["result"]["source_query"] == "why crash?"
|
|
91
|
+
|
|
92
|
+
def test_create_auto_attaches_decisions(self, workspace):
|
|
93
|
+
_run(workspace, "intent", "create", "Goal A", "--query", "q")
|
|
94
|
+
_run(workspace, "decision", "create", "Rule 1", "--rationale", "r")
|
|
95
|
+
r = _run(workspace, "intent", "create", "Goal B", "--query", "q")
|
|
96
|
+
assert "decision-001" in r["result"]["decision_ids"]
|
|
97
|
+
|
|
98
|
+
def test_list(self, workspace):
|
|
99
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
100
|
+
_run(workspace, "intent", "create", "B", "--query", "q")
|
|
101
|
+
r = _run(workspace, "intent", "list")
|
|
102
|
+
assert len(r["result"]) == 2
|
|
103
|
+
|
|
104
|
+
def test_list_filter_status(self, workspace):
|
|
105
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
106
|
+
_run(workspace, "intent", "create", "B", "--query", "q")
|
|
107
|
+
_run(workspace, "intent", "suspend", "intent-001")
|
|
108
|
+
r = _run(workspace, "intent", "list", "--status", "active")
|
|
109
|
+
assert len(r["result"]) == 1
|
|
110
|
+
assert r["result"][0]["id"] == "intent-002"
|
|
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
|
+
|
|
126
|
+
def test_show(self, workspace):
|
|
127
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
128
|
+
r = _run(workspace, "intent", "show", "intent-001")
|
|
129
|
+
assert r["result"]["title"] == "A"
|
|
130
|
+
|
|
131
|
+
def test_show_not_found(self, workspace):
|
|
132
|
+
r = _run(workspace, "intent", "show", "intent-999")
|
|
133
|
+
assert r["error"]["code"] == "OBJECT_NOT_FOUND"
|
|
134
|
+
|
|
135
|
+
def test_suspend_activate(self, workspace):
|
|
136
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
137
|
+
r = _run(workspace, "intent", "suspend", "intent-001")
|
|
138
|
+
assert r["result"]["status"] == "suspend"
|
|
139
|
+
r = _run(workspace, "intent", "activate", "intent-001")
|
|
140
|
+
assert r["result"]["status"] == "active"
|
|
141
|
+
|
|
142
|
+
def test_activate_catches_up_decisions(self, workspace):
|
|
143
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
144
|
+
_run(workspace, "intent", "suspend", "intent-001")
|
|
145
|
+
_run(workspace, "decision", "create", "New rule", "--rationale", "r")
|
|
146
|
+
r = _run(workspace, "intent", "activate", "intent-001")
|
|
147
|
+
assert "decision-001" in r["result"]["decision_ids"]
|
|
148
|
+
|
|
149
|
+
def test_done(self, workspace):
|
|
150
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
151
|
+
r = _run(workspace, "intent", "done", "intent-001")
|
|
152
|
+
assert r["result"]["status"] == "done"
|
|
153
|
+
|
|
154
|
+
def test_done_is_terminal(self, workspace):
|
|
155
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
156
|
+
_run(workspace, "intent", "done", "intent-001")
|
|
157
|
+
r = _run(workspace, "intent", "activate", "intent-001")
|
|
158
|
+
assert r["error"]["code"] == "STATE_CONFLICT"
|
|
159
|
+
|
|
160
|
+
def test_suspend_only_active(self, workspace):
|
|
161
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
162
|
+
_run(workspace, "intent", "done", "intent-001")
|
|
163
|
+
r = _run(workspace, "intent", "suspend", "intent-001")
|
|
164
|
+
assert r["error"]["code"] == "STATE_CONFLICT"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Snap commands
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
class TestSnap:
|
|
172
|
+
def test_create(self, workspace):
|
|
173
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
174
|
+
r = _run(workspace, "snap", "create", "Did X", "--intent", "intent-001",
|
|
175
|
+
"--summary", "details")
|
|
176
|
+
assert r["ok"] is True
|
|
177
|
+
assert r["result"]["id"] == "snap-001"
|
|
178
|
+
assert r["result"]["intent_id"] == "intent-001"
|
|
179
|
+
|
|
180
|
+
def test_create_updates_intent_snap_ids(self, workspace):
|
|
181
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
182
|
+
_run(workspace, "snap", "create", "S1", "--intent", "intent-001")
|
|
183
|
+
_run(workspace, "snap", "create", "S2", "--intent", "intent-001")
|
|
184
|
+
r = _run(workspace, "intent", "show", "intent-001")
|
|
185
|
+
assert r["result"]["snap_ids"] == ["snap-001", "snap-002"]
|
|
186
|
+
|
|
187
|
+
def test_create_requires_active_intent(self, workspace):
|
|
188
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
189
|
+
_run(workspace, "intent", "done", "intent-001")
|
|
190
|
+
r = _run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
191
|
+
assert r["error"]["code"] == "STATE_CONFLICT"
|
|
192
|
+
|
|
193
|
+
def test_create_intent_not_found(self, workspace):
|
|
194
|
+
r = _run(workspace, "snap", "create", "S", "--intent", "intent-999")
|
|
195
|
+
assert r["error"]["code"] == "OBJECT_NOT_FOUND"
|
|
196
|
+
|
|
197
|
+
def test_list(self, workspace):
|
|
198
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
199
|
+
_run(workspace, "snap", "create", "S1", "--intent", "intent-001")
|
|
200
|
+
_run(workspace, "snap", "create", "S2", "--intent", "intent-001")
|
|
201
|
+
r = _run(workspace, "snap", "list")
|
|
202
|
+
assert len(r["result"]) == 2
|
|
203
|
+
|
|
204
|
+
def test_list_filter_intent(self, workspace):
|
|
205
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
206
|
+
_run(workspace, "intent", "create", "B", "--query", "q")
|
|
207
|
+
_run(workspace, "snap", "create", "S1", "--intent", "intent-001")
|
|
208
|
+
_run(workspace, "snap", "create", "S2", "--intent", "intent-002")
|
|
209
|
+
r = _run(workspace, "snap", "list", "--intent", "intent-002")
|
|
210
|
+
assert len(r["result"]) == 1
|
|
211
|
+
assert r["result"][0]["id"] == "snap-002"
|
|
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
|
+
|
|
218
|
+
def test_feedback(self, workspace):
|
|
219
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
220
|
+
_run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
221
|
+
r = _run(workspace, "snap", "feedback", "snap-001", "looks good")
|
|
222
|
+
assert r["result"]["feedback"] == "looks good"
|
|
223
|
+
|
|
224
|
+
def test_feedback_overwrites(self, workspace):
|
|
225
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
226
|
+
_run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
227
|
+
_run(workspace, "snap", "feedback", "snap-001", "first")
|
|
228
|
+
r = _run(workspace, "snap", "feedback", "snap-001", "second")
|
|
229
|
+
assert r["result"]["feedback"] == "second"
|
|
230
|
+
|
|
231
|
+
def test_revert(self, workspace):
|
|
232
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
233
|
+
_run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
234
|
+
r = _run(workspace, "snap", "revert", "snap-001")
|
|
235
|
+
assert r["result"]["status"] == "reverted"
|
|
236
|
+
|
|
237
|
+
def test_revert_is_terminal(self, workspace):
|
|
238
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
239
|
+
_run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
240
|
+
_run(workspace, "snap", "revert", "snap-001")
|
|
241
|
+
r = _run(workspace, "snap", "revert", "snap-001")
|
|
242
|
+
assert r["error"]["code"] == "STATE_CONFLICT"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# Decision commands
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
class TestDecision:
|
|
250
|
+
def test_create(self, workspace):
|
|
251
|
+
r = _run(workspace, "decision", "create", "Rule", "--rationale", "reason")
|
|
252
|
+
assert r["ok"] is True
|
|
253
|
+
assert r["result"]["id"] == "decision-001"
|
|
254
|
+
assert r["result"]["status"] == "active"
|
|
255
|
+
|
|
256
|
+
def test_create_auto_attaches_intents(self, workspace):
|
|
257
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
258
|
+
r = _run(workspace, "decision", "create", "Rule", "--rationale", "r")
|
|
259
|
+
assert "intent-001" in r["result"]["intent_ids"]
|
|
260
|
+
# Verify bidirectional
|
|
261
|
+
i = _run(workspace, "intent", "show", "intent-001")
|
|
262
|
+
assert "decision-001" in i["result"]["decision_ids"]
|
|
263
|
+
|
|
264
|
+
def test_list(self, workspace):
|
|
265
|
+
_run(workspace, "decision", "create", "R1", "--rationale", "r")
|
|
266
|
+
_run(workspace, "decision", "create", "R2", "--rationale", "r")
|
|
267
|
+
r = _run(workspace, "decision", "list")
|
|
268
|
+
assert len(r["result"]) == 2
|
|
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
|
+
|
|
286
|
+
def test_deprecate(self, workspace):
|
|
287
|
+
_run(workspace, "decision", "create", "R", "--rationale", "r")
|
|
288
|
+
r = _run(workspace, "decision", "deprecate", "decision-001")
|
|
289
|
+
assert r["result"]["status"] == "deprecated"
|
|
290
|
+
|
|
291
|
+
def test_deprecate_is_terminal(self, workspace):
|
|
292
|
+
_run(workspace, "decision", "create", "R", "--rationale", "r")
|
|
293
|
+
_run(workspace, "decision", "deprecate", "decision-001")
|
|
294
|
+
r = _run(workspace, "decision", "deprecate", "decision-001")
|
|
295
|
+
assert r["error"]["code"] == "STATE_CONFLICT"
|
|
296
|
+
|
|
297
|
+
def test_deprecated_not_auto_attached(self, workspace):
|
|
298
|
+
_run(workspace, "decision", "create", "R", "--rationale", "r")
|
|
299
|
+
_run(workspace, "decision", "deprecate", "decision-001")
|
|
300
|
+
r = _run(workspace, "intent", "create", "New goal", "--query", "q")
|
|
301
|
+
assert "decision-001" not in r["result"]["decision_ids"]
|
|
302
|
+
|
|
303
|
+
def test_attach(self, workspace):
|
|
304
|
+
_run(workspace, "intent", "create", "A", "--query", "q")
|
|
305
|
+
_run(workspace, "intent", "create", "B", "--query", "q")
|
|
306
|
+
_run(workspace, "decision", "create", "R", "--rationale", "r")
|
|
307
|
+
# decision-001 auto-attached to both. Manually attach to verify idempotency.
|
|
308
|
+
r = _run(workspace, "decision", "attach", "decision-001", "--intent", "intent-001")
|
|
309
|
+
assert r["ok"] is True
|
|
310
|
+
|
|
311
|
+
def test_attach_not_found(self, workspace):
|
|
312
|
+
_run(workspace, "decision", "create", "R", "--rationale", "r")
|
|
313
|
+
r = _run(workspace, "decision", "attach", "decision-001", "--intent", "intent-999")
|
|
314
|
+
assert r["error"]["code"] == "OBJECT_NOT_FOUND"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Inspect
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
class TestInspect:
|
|
322
|
+
def test_full_graph(self, workspace):
|
|
323
|
+
_run(workspace, "intent", "create", "Active", "--query", "q")
|
|
324
|
+
_run(workspace, "intent", "create", "Will suspend", "--query", "q")
|
|
325
|
+
_run(workspace, "intent", "suspend", "intent-002")
|
|
326
|
+
_run(workspace, "decision", "create", "Rule", "--rationale", "r")
|
|
327
|
+
_run(workspace, "snap", "create", "S1", "--intent", "intent-001",
|
|
328
|
+
"--summary", "did something")
|
|
329
|
+
|
|
330
|
+
r = _run(workspace, "inspect")
|
|
331
|
+
assert r["ok"] is True
|
|
332
|
+
assert len(r["active_intents"]) == 1
|
|
333
|
+
assert r["active_intents"][0]["id"] == "intent-001"
|
|
334
|
+
assert r["active_intents"][0]["latest_snap_id"] == "snap-001"
|
|
335
|
+
assert len(r["suspend_intents"]) == 1
|
|
336
|
+
assert r["suspend_intents"][0]["id"] == "intent-002"
|
|
337
|
+
assert len(r["active_decisions"]) == 1
|
|
338
|
+
assert len(r["recent_snaps"]) == 1
|
|
339
|
+
|
|
340
|
+
def test_orphan_snap_warning(self, workspace):
|
|
341
|
+
_run(workspace, "intent", "create", "Goal", "--query", "q")
|
|
342
|
+
_run(workspace, "snap", "create", "S", "--intent", "intent-001")
|
|
343
|
+
# Delete intent file to create orphan
|
|
344
|
+
intent_file = workspace / ".intent" / "intents" / "intent-001.json"
|
|
345
|
+
intent_file.unlink()
|
|
346
|
+
r = _run(workspace, "inspect")
|
|
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": "0.6"}, 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
|
|
File without changes
|
{intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{intent_cli_python-0.6.0 → intent_cli_python-1.1.0}/src/intent_cli_python.egg-info/top_level.txt
RENAMED
|
File without changes
|