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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-intent
3
- Version: 0.3.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-intent"
7
- version = "0.3.1"
7
+ version = "0.3.2"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-intent
3
- Version: 0.3.1
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
@@ -26,4 +26,4 @@ if __version__ is None:
26
26
  try:
27
27
  __version__ = metadata.version(PACKAGE_NAME)
28
28
  except metadata.PackageNotFoundError:
29
- __version__ = "0.3.1"
29
+ __version__ = "0.3.2"
@@ -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