git-intent 0.3.1__tar.gz → 0.3.2__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.
- {git_intent-0.3.1/src/git_intent.egg-info → git_intent-0.3.2}/PKG-INFO +6 -4
- {git_intent-0.3.1 → git_intent-0.3.2}/README.md +5 -3
- {git_intent-0.3.1 → git_intent-0.3.2}/pyproject.toml +1 -1
- {git_intent-0.3.1 → git_intent-0.3.2/src/git_intent.egg-info}/PKG-INFO +6 -4
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/__init__.py +1 -1
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/cli.py +15 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/core.py +86 -2
- {git_intent-0.3.1 → git_intent-0.3.2}/tests/test_cli.py +62 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/LICENSE +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/setup.cfg +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/git_intent.egg-info/SOURCES.txt +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/git_intent.egg-info/dependency_links.txt +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/git_intent.egg-info/entry_points.txt +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/git_intent.egg-info/top_level.txt +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/__main__.py +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/constants.py +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/errors.py +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/git.py +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/helpers.py +0 -0
- {git_intent-0.3.1 → git_intent-0.3.2}/src/intent_cli/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-intent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Dynamic: license-file
|
|
24
24
|
|
|
25
|
-
English | [简体中文](README.CN.md)
|
|
25
|
+
English | [简体中文](https://github.com/dozybot001/Intent/blob/main/README.CN.md)
|
|
26
26
|
|
|
27
27
|
# Intent
|
|
28
28
|
|
|
@@ -113,10 +113,12 @@ pip install -e .
|
|
|
113
113
|
| `itt inspect` | Machine-readable workspace snapshot |
|
|
114
114
|
| `itt list <intent\|snap>` | List objects |
|
|
115
115
|
| `itt show <id>` | Show a single object |
|
|
116
|
+
| `itt suspend` | Suspend the active intent |
|
|
117
|
+
| `itt resume [id]` | Resume a suspended intent |
|
|
116
118
|
| `itt adopt [id]` | Adopt a candidate snap |
|
|
117
119
|
| `itt revert` | Revert the latest snap |
|
|
118
120
|
|
|
119
121
|
## Documentation
|
|
120
122
|
|
|
121
|
-
- [CLI spec](docs/cli.EN.md) — objects, commands, JSON output contract
|
|
122
|
-
- [Agent integration](docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
123
|
+
- [CLI spec](https://github.com/dozybot001/Intent/blob/main/docs/cli.EN.md) — objects, commands, JSON output contract
|
|
124
|
+
- [Agent integration](https://github.com/dozybot001/Intent/blob/main/docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
English | [简体中文](README.CN.md)
|
|
1
|
+
English | [简体中文](https://github.com/dozybot001/Intent/blob/main/README.CN.md)
|
|
2
2
|
|
|
3
3
|
# Intent
|
|
4
4
|
|
|
@@ -89,10 +89,12 @@ pip install -e .
|
|
|
89
89
|
| `itt inspect` | Machine-readable workspace snapshot |
|
|
90
90
|
| `itt list <intent\|snap>` | List objects |
|
|
91
91
|
| `itt show <id>` | Show a single object |
|
|
92
|
+
| `itt suspend` | Suspend the active intent |
|
|
93
|
+
| `itt resume [id]` | Resume a suspended intent |
|
|
92
94
|
| `itt adopt [id]` | Adopt a candidate snap |
|
|
93
95
|
| `itt revert` | Revert the latest snap |
|
|
94
96
|
|
|
95
97
|
## Documentation
|
|
96
98
|
|
|
97
|
-
- [CLI spec](docs/cli.EN.md) — objects, commands, JSON output contract
|
|
98
|
-
- [Agent integration](docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
99
|
+
- [CLI spec](https://github.com/dozybot001/Intent/blob/main/docs/cli.EN.md) — objects, commands, JSON output contract
|
|
100
|
+
- [Agent integration](https://github.com/dozybot001/Intent/blob/main/docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-intent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Dynamic: license-file
|
|
24
24
|
|
|
25
|
-
English | [简体中文](README.CN.md)
|
|
25
|
+
English | [简体中文](https://github.com/dozybot001/Intent/blob/main/README.CN.md)
|
|
26
26
|
|
|
27
27
|
# Intent
|
|
28
28
|
|
|
@@ -113,10 +113,12 @@ pip install -e .
|
|
|
113
113
|
| `itt inspect` | Machine-readable workspace snapshot |
|
|
114
114
|
| `itt list <intent\|snap>` | List objects |
|
|
115
115
|
| `itt show <id>` | Show a single object |
|
|
116
|
+
| `itt suspend` | Suspend the active intent |
|
|
117
|
+
| `itt resume [id]` | Resume a suspended intent |
|
|
116
118
|
| `itt adopt [id]` | Adopt a candidate snap |
|
|
117
119
|
| `itt revert` | Revert the latest snap |
|
|
118
120
|
|
|
119
121
|
## Documentation
|
|
120
122
|
|
|
121
|
-
- [CLI spec](docs/cli.EN.md) — objects, commands, JSON output contract
|
|
122
|
-
- [Agent integration](docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
123
|
+
- [CLI spec](https://github.com/dozybot001/Intent/blob/main/docs/cli.EN.md) — objects, commands, JSON output contract
|
|
124
|
+
- [Agent integration](https://github.com/dozybot001/Intent/blob/main/docs/agent-integration.md) — copy-paste snippets for Claude Code, Cursor, AGENTS.md
|
|
@@ -48,6 +48,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
48
48
|
revert_p = sub.add_parser("revert", help="Revert the latest adopted snap")
|
|
49
49
|
revert_p.add_argument("-m", "--message", help="Rationale for revert")
|
|
50
50
|
|
|
51
|
+
sub.add_parser("suspend", help="Suspend the active intent")
|
|
52
|
+
|
|
53
|
+
resume_p = sub.add_parser("resume", help="Resume a suspended intent")
|
|
54
|
+
resume_p.add_argument("intent_id", nargs="?")
|
|
55
|
+
|
|
51
56
|
done_p = sub.add_parser("done", help="Close the active intent")
|
|
52
57
|
done_p.add_argument("intent_id", nargs="?")
|
|
53
58
|
|
|
@@ -105,6 +110,16 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
105
110
|
emit(ok("revert", snap, warnings=warnings))
|
|
106
111
|
return EXIT_SUCCESS
|
|
107
112
|
|
|
113
|
+
if args.command == "suspend":
|
|
114
|
+
intent, warnings = repo.suspend_intent()
|
|
115
|
+
emit(ok("suspend", intent, warnings=warnings))
|
|
116
|
+
return EXIT_SUCCESS
|
|
117
|
+
|
|
118
|
+
if args.command == "resume":
|
|
119
|
+
intent, warnings = repo.resume_intent(intent_id=args.intent_id)
|
|
120
|
+
emit(ok("resume", intent, warnings=warnings))
|
|
121
|
+
return EXIT_SUCCESS
|
|
122
|
+
|
|
108
123
|
if args.command == "done":
|
|
109
124
|
intent, warnings = repo.close_intent(intent_id=args.intent_id)
|
|
110
125
|
emit(ok("done", intent, warnings=warnings))
|
|
@@ -92,7 +92,7 @@ class IntentRepository:
|
|
|
92
92
|
EXIT_STATE_CONFLICT,
|
|
93
93
|
"STATE_CONFLICT",
|
|
94
94
|
f"Intent '{current['id']}' is still open.",
|
|
95
|
-
suggested_fix="itt done",
|
|
95
|
+
suggested_fix="itt done or itt suspend",
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
intent_id = self.store.next_id("intent")
|
|
@@ -141,6 +141,77 @@ class IntentRepository:
|
|
|
141
141
|
|
|
142
142
|
return intent, []
|
|
143
143
|
|
|
144
|
+
def suspend_intent(self) -> Tuple[Dict[str, Any], List[str]]:
|
|
145
|
+
self.ensure_git()
|
|
146
|
+
self.ensure_initialized()
|
|
147
|
+
state = self._load_state()
|
|
148
|
+
intent = self._require_active_intent(state)
|
|
149
|
+
|
|
150
|
+
intent["status"] = "suspended"
|
|
151
|
+
intent["updated_at"] = utc_now()
|
|
152
|
+
self.store.save_object("intent", intent)
|
|
153
|
+
|
|
154
|
+
state["active_intent_id"] = None
|
|
155
|
+
state["workspace_status"] = "idle"
|
|
156
|
+
self._save_state(state)
|
|
157
|
+
return intent, []
|
|
158
|
+
|
|
159
|
+
def resume_intent(self, intent_id: Optional[str] = None) -> Tuple[Dict[str, Any], List[str]]:
|
|
160
|
+
self.ensure_git()
|
|
161
|
+
self.ensure_initialized()
|
|
162
|
+
state = self._load_state()
|
|
163
|
+
|
|
164
|
+
current = self._active_intent(state)
|
|
165
|
+
if current and current.get("status") == "open":
|
|
166
|
+
raise IntentError(
|
|
167
|
+
EXIT_STATE_CONFLICT,
|
|
168
|
+
"STATE_CONFLICT",
|
|
169
|
+
f"Intent '{current['id']}' is still open.",
|
|
170
|
+
suggested_fix="itt suspend or itt done",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
suspended = [
|
|
174
|
+
i for i in self.store.list_objects("intent")
|
|
175
|
+
if i.get("status") == "suspended"
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
if intent_id:
|
|
179
|
+
intent = self.store.require_object("intent", intent_id)
|
|
180
|
+
if intent.get("status") != "suspended":
|
|
181
|
+
raise IntentError(
|
|
182
|
+
EXIT_STATE_CONFLICT,
|
|
183
|
+
"STATE_CONFLICT",
|
|
184
|
+
f"Intent '{intent_id}' is not suspended.",
|
|
185
|
+
)
|
|
186
|
+
elif len(suspended) == 1:
|
|
187
|
+
intent = suspended[0]
|
|
188
|
+
elif len(suspended) == 0:
|
|
189
|
+
raise IntentError(
|
|
190
|
+
EXIT_STATE_CONFLICT,
|
|
191
|
+
"STATE_CONFLICT",
|
|
192
|
+
"No suspended intents to resume.",
|
|
193
|
+
suggested_fix='itt start "Describe the problem"',
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
raise IntentError(
|
|
197
|
+
EXIT_STATE_CONFLICT,
|
|
198
|
+
"STATE_CONFLICT",
|
|
199
|
+
"Multiple suspended intents. Specify which one to resume.",
|
|
200
|
+
details={
|
|
201
|
+
"suspended": [{"id": i["id"], "title": i["title"]} for i in suspended],
|
|
202
|
+
},
|
|
203
|
+
suggested_fix=f"itt resume {suspended[-1]['id']}",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
intent["status"] = "open"
|
|
207
|
+
intent["updated_at"] = utc_now()
|
|
208
|
+
self.store.save_object("intent", intent)
|
|
209
|
+
|
|
210
|
+
state["active_intent_id"] = intent["id"]
|
|
211
|
+
state["workspace_status"] = "active"
|
|
212
|
+
self._save_state(state)
|
|
213
|
+
return intent, []
|
|
214
|
+
|
|
144
215
|
# --- snap lifecycle ---
|
|
145
216
|
|
|
146
217
|
def create_snap(
|
|
@@ -276,6 +347,12 @@ class IntentRepository:
|
|
|
276
347
|
for c in self._candidate_snaps(intent["id"])
|
|
277
348
|
]
|
|
278
349
|
|
|
350
|
+
suspended_intents = [
|
|
351
|
+
{"id": i["id"], "title": i["title"]}
|
|
352
|
+
for i in self.store.list_objects("intent")
|
|
353
|
+
if i.get("status") == "suspended"
|
|
354
|
+
]
|
|
355
|
+
|
|
279
356
|
workspace_status = self._derive_workspace_status(state)
|
|
280
357
|
if workspace_status != state.get("workspace_status"):
|
|
281
358
|
state["workspace_status"] = workspace_status
|
|
@@ -286,7 +363,7 @@ class IntentRepository:
|
|
|
286
363
|
return None
|
|
287
364
|
return {k: obj[k] for k in ("id", "title", "status", "rationale") if k in obj}
|
|
288
365
|
|
|
289
|
-
action = self._next_action(intent, candidate_snaps)
|
|
366
|
+
action = self._next_action(intent, candidate_snaps, suspended_intents)
|
|
290
367
|
|
|
291
368
|
return {
|
|
292
369
|
"ok": True,
|
|
@@ -295,6 +372,7 @@ class IntentRepository:
|
|
|
295
372
|
"intent": brief(intent),
|
|
296
373
|
"latest_snap": brief(latest_snap),
|
|
297
374
|
"candidate_snaps": candidate_snaps,
|
|
375
|
+
"suspended_intents": suspended_intents,
|
|
298
376
|
"suggested_next_action": action,
|
|
299
377
|
"git": {
|
|
300
378
|
"branch": git_payload["branch"],
|
|
@@ -340,8 +418,14 @@ class IntentRepository:
|
|
|
340
418
|
self,
|
|
341
419
|
intent: Optional[Dict[str, Any]],
|
|
342
420
|
candidates: List[Dict[str, Any]],
|
|
421
|
+
suspended: Optional[List[Dict[str, Any]]] = None,
|
|
343
422
|
) -> Optional[Dict[str, Any]]:
|
|
344
423
|
if not intent or intent.get("status") != "open":
|
|
424
|
+
if suspended:
|
|
425
|
+
return {
|
|
426
|
+
"command": f"itt resume {suspended[-1]['id']}",
|
|
427
|
+
"reason": "Suspended intents exist.",
|
|
428
|
+
}
|
|
345
429
|
return {"command": "itt start 'Describe the problem'", "reason": "No active intent."}
|
|
346
430
|
if len(candidates) > 1:
|
|
347
431
|
return {
|
|
@@ -201,6 +201,66 @@ class IntentCliTests(unittest.TestCase):
|
|
|
201
201
|
d, rc = self.itt_rc("done")
|
|
202
202
|
self.assertFalse(d["ok"])
|
|
203
203
|
|
|
204
|
+
# --- suspend and resume ---
|
|
205
|
+
|
|
206
|
+
def test_suspend_and_resume(self):
|
|
207
|
+
self.itt("init")
|
|
208
|
+
self.itt("start", "Task A")
|
|
209
|
+
|
|
210
|
+
# suspend
|
|
211
|
+
d = self.itt("suspend")
|
|
212
|
+
self.assertEqual(d["result"]["status"], "suspended")
|
|
213
|
+
|
|
214
|
+
# workspace is idle
|
|
215
|
+
d = self.itt("inspect")
|
|
216
|
+
self.assertEqual(d["workspace_status"], "idle")
|
|
217
|
+
self.assertEqual(len(d["suspended_intents"]), 1)
|
|
218
|
+
|
|
219
|
+
# can start a new intent
|
|
220
|
+
self.itt("start", "Task B")
|
|
221
|
+
d = self.itt("inspect")
|
|
222
|
+
self.assertEqual(d["intent"]["title"], "Task B")
|
|
223
|
+
|
|
224
|
+
# done Task B
|
|
225
|
+
self.itt("done")
|
|
226
|
+
|
|
227
|
+
# resume Task A
|
|
228
|
+
d = self.itt("resume")
|
|
229
|
+
self.assertEqual(d["result"]["status"], "open")
|
|
230
|
+
d = self.itt("inspect")
|
|
231
|
+
self.assertEqual(d["intent"]["title"], "Task A")
|
|
232
|
+
|
|
233
|
+
def test_suspend_when_idle_fails(self):
|
|
234
|
+
self.itt("init")
|
|
235
|
+
d, rc = self.itt_rc("suspend")
|
|
236
|
+
self.assertFalse(d["ok"])
|
|
237
|
+
|
|
238
|
+
def test_resume_when_active_fails(self):
|
|
239
|
+
self.itt("init")
|
|
240
|
+
self.itt("start", "A")
|
|
241
|
+
self.itt("suspend")
|
|
242
|
+
self.itt("start", "B")
|
|
243
|
+
d, rc = self.itt_rc("resume", "intent-001")
|
|
244
|
+
self.assertFalse(d["ok"])
|
|
245
|
+
self.assertEqual(d["error"]["code"], "STATE_CONFLICT")
|
|
246
|
+
|
|
247
|
+
def test_resume_multiple_requires_id(self):
|
|
248
|
+
self.itt("init")
|
|
249
|
+
self.itt("start", "A")
|
|
250
|
+
self.itt("suspend")
|
|
251
|
+
self.itt("start", "B")
|
|
252
|
+
self.itt("suspend")
|
|
253
|
+
|
|
254
|
+
# resume without id fails
|
|
255
|
+
d, rc = self.itt_rc("resume")
|
|
256
|
+
self.assertFalse(d["ok"])
|
|
257
|
+
self.assertIn("candidates" if False else "suspended", str(d["error"].get("details", {})))
|
|
258
|
+
|
|
259
|
+
# resume with id works
|
|
260
|
+
d = self.itt("resume", "intent-001")
|
|
261
|
+
self.assertEqual(d["result"]["id"], "intent-001")
|
|
262
|
+
self.assertEqual(d["result"]["status"], "open")
|
|
263
|
+
|
|
204
264
|
# --- list and show ---
|
|
205
265
|
|
|
206
266
|
def test_list_and_show(self):
|
|
@@ -239,6 +299,8 @@ class IntentCliTests(unittest.TestCase):
|
|
|
239
299
|
["snap", "S2", "--candidate"],
|
|
240
300
|
["adopt"],
|
|
241
301
|
["revert"],
|
|
302
|
+
["suspend"],
|
|
303
|
+
["resume"],
|
|
242
304
|
["list", "intent"],
|
|
243
305
|
["list", "snap"],
|
|
244
306
|
["show", "intent-001"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|