intent-cli-python 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/PKG-INFO +5 -1
  2. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/README.md +4 -0
  3. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/pyproject.toml +1 -1
  4. intent_cli_python-1.2.0/src/intent_cli/cli.py +178 -0
  5. intent_cli_python-1.2.0/src/intent_cli/commands/__init__.py +1 -0
  6. intent_cli_python-1.2.0/src/intent_cli/commands/common.py +41 -0
  7. intent_cli_python-1.2.0/src/intent_cli/commands/core.py +376 -0
  8. intent_cli_python-1.2.0/src/intent_cli/commands/hub.py +107 -0
  9. intent_cli_python-1.2.0/src/intent_cli/hub/__init__.py +1 -0
  10. intent_cli_python-1.2.0/src/intent_cli/hub/client.py +59 -0
  11. intent_cli_python-1.2.0/src/intent_cli/hub/payload.py +65 -0
  12. intent_cli_python-1.2.0/src/intent_cli/hub/runtime.py +40 -0
  13. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli/store.py +96 -0
  14. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli_python.egg-info/PKG-INFO +5 -1
  15. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli_python.egg-info/SOURCES.txt +8 -0
  16. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/tests/test_cli.py +98 -0
  17. intent_cli_python-1.1.0/src/intent_cli/cli.py +0 -528
  18. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/LICENSE +0 -0
  19. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/setup.cfg +0 -0
  20. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli/__init__.py +0 -0
  21. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli/__main__.py +0 -0
  22. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli/output.py +0 -0
  23. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli_python.egg-info/dependency_links.txt +0 -0
  24. {intent_cli_python-1.1.0 → intent_cli_python-1.2.0}/src/intent_cli_python.egg-info/entry_points.txt +0 -0
  25. {intent_cli_python-1.1.0 → intent_cli_python-1.2.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.1.0
3
+ Version: 1.2.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
@@ -173,11 +173,15 @@ All data lives in `.intent/` at your git repo root:
173
173
  decision-001.json
174
174
  ```
175
175
 
176
+ `.intent/` is local semantic-workspace metadata. It should stay out of Git history and should remain ignored by `.gitignore`.
177
+
176
178
  ## Docs
177
179
 
178
180
  - [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
179
181
  - [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
180
182
  - [Roadmap](docs/EN/roadmap.md) — phase plan
183
+ - [IntHub MVP](docs/EN/inthub-mvp.md) — first remote collaboration-layer scope
184
+ - [IntHub Sync Contract](docs/EN/inthub-sync-contract.md) — first sync, identity, and API contract
181
185
 
182
186
  ## License
183
187
 
@@ -149,11 +149,15 @@ All data lives in `.intent/` at your git repo root:
149
149
  decision-001.json
150
150
  ```
151
151
 
152
+ `.intent/` is local semantic-workspace metadata. It should stay out of Git history and should remain ignored by `.gitignore`.
153
+
152
154
  ## Docs
153
155
 
154
156
  - [Vision](docs/EN/vision.md) — why semantic history matters. **If this project interests you, start here.**
155
157
  - [CLI Design](docs/EN/cli.md) — object model, commands, JSON contract
156
158
  - [Roadmap](docs/EN/roadmap.md) — phase plan
159
+ - [IntHub MVP](docs/EN/inthub-mvp.md) — first remote collaboration-layer scope
160
+ - [IntHub Sync Contract](docs/EN/inthub-sync-contract.md) — first sync, identity, and API contract
157
161
 
158
162
  ## License
159
163
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intent-cli-python"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Semantic history for agent-driven development. Records what you did and why."
9
9
  requires-python = ">=3.9"
10
10
  readme = "README.md"
@@ -0,0 +1,178 @@
1
+ """Intent CLI — parser and command dispatch."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from intent_cli.commands.core import (
7
+ cmd_decision_attach,
8
+ cmd_decision_create,
9
+ cmd_decision_deprecate,
10
+ cmd_decision_list,
11
+ cmd_decision_show,
12
+ cmd_doctor,
13
+ cmd_init,
14
+ cmd_inspect,
15
+ cmd_intent_activate,
16
+ cmd_intent_create,
17
+ cmd_intent_done,
18
+ cmd_intent_list,
19
+ cmd_intent_show,
20
+ cmd_intent_suspend,
21
+ cmd_snap_create,
22
+ cmd_snap_feedback,
23
+ cmd_snap_list,
24
+ cmd_snap_revert,
25
+ cmd_snap_show,
26
+ cmd_version,
27
+ )
28
+ from intent_cli.commands.hub import cmd_hub_link, cmd_hub_login, cmd_hub_sync
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(prog="itt", description="Intent CLI")
33
+ sub = parser.add_subparsers(dest="command")
34
+
35
+ # version / init / inspect / doctor
36
+ sub.add_parser("version")
37
+ sub.add_parser("init")
38
+ sub.add_parser("inspect")
39
+ sub.add_parser("doctor")
40
+
41
+ # --- hub ---
42
+ p_hub = sub.add_parser("hub")
43
+ s_hub = p_hub.add_subparsers(dest="sub")
44
+
45
+ p = s_hub.add_parser("login")
46
+ p.add_argument("--api-base-url", default=None)
47
+ p.add_argument("--token", default=None)
48
+
49
+ p = s_hub.add_parser("link")
50
+ p.add_argument("--project-name", default=None)
51
+ p.add_argument("--api-base-url", default=None)
52
+ p.add_argument("--token", default=None)
53
+
54
+ p = s_hub.add_parser("sync")
55
+ p.add_argument("--api-base-url", default=None)
56
+ p.add_argument("--token", default=None)
57
+ p.add_argument("--dry-run", action="store_true")
58
+
59
+ # --- intent ---
60
+ p_intent = sub.add_parser("intent")
61
+ s_intent = p_intent.add_subparsers(dest="sub")
62
+
63
+ p = s_intent.add_parser("create")
64
+ p.add_argument("title")
65
+ p.add_argument("--query", default="")
66
+ p.add_argument("--rationale", default="")
67
+
68
+ p = s_intent.add_parser("list")
69
+ p.add_argument("--status", default=None)
70
+ p.add_argument("--decision", default=None)
71
+
72
+ p = s_intent.add_parser("show")
73
+ p.add_argument("id")
74
+
75
+ p = s_intent.add_parser("activate")
76
+ p.add_argument("id")
77
+
78
+ p = s_intent.add_parser("suspend")
79
+ p.add_argument("id")
80
+
81
+ p = s_intent.add_parser("done")
82
+ p.add_argument("id")
83
+
84
+ # --- snap ---
85
+ p_snap = sub.add_parser("snap")
86
+ s_snap = p_snap.add_subparsers(dest="sub")
87
+
88
+ p = s_snap.add_parser("create")
89
+ p.add_argument("title")
90
+ p.add_argument("--intent", required=True)
91
+ p.add_argument("--query", default="")
92
+ p.add_argument("--rationale", default="")
93
+ p.add_argument("--summary", default="")
94
+ p.add_argument("--feedback", default="")
95
+
96
+ p = s_snap.add_parser("list")
97
+ p.add_argument("--intent", default=None)
98
+ p.add_argument("--status", default=None)
99
+
100
+ p = s_snap.add_parser("show")
101
+ p.add_argument("id")
102
+
103
+ p = s_snap.add_parser("feedback")
104
+ p.add_argument("id")
105
+ p.add_argument("feedback")
106
+
107
+ p = s_snap.add_parser("revert")
108
+ p.add_argument("id")
109
+
110
+ # --- decision ---
111
+ p_decision = sub.add_parser("decision")
112
+ s_decision = p_decision.add_subparsers(dest="sub")
113
+
114
+ p = s_decision.add_parser("create")
115
+ p.add_argument("title")
116
+ p.add_argument("--rationale", default="")
117
+
118
+ p = s_decision.add_parser("list")
119
+ p.add_argument("--status", default=None)
120
+ p.add_argument("--intent", default=None)
121
+
122
+ p = s_decision.add_parser("show")
123
+ p.add_argument("id")
124
+
125
+ p = s_decision.add_parser("deprecate")
126
+ p.add_argument("id")
127
+
128
+ p = s_decision.add_parser("attach")
129
+ p.add_argument("id")
130
+ p.add_argument("--intent", required=True)
131
+
132
+ args = parser.parse_args()
133
+
134
+ if args.command is None:
135
+ parser.print_help()
136
+ sys.exit(1)
137
+
138
+ dispatch_global = {
139
+ "version": cmd_version,
140
+ "init": cmd_init,
141
+ "inspect": cmd_inspect,
142
+ "doctor": cmd_doctor,
143
+ }
144
+ if args.command in dispatch_global:
145
+ dispatch_global[args.command](args)
146
+ return
147
+
148
+ if not getattr(args, "sub", None):
149
+ {
150
+ "hub": p_hub,
151
+ "intent": p_intent,
152
+ "snap": p_snap,
153
+ "decision": p_decision,
154
+ }[args.command].print_help()
155
+ sys.exit(1)
156
+
157
+ dispatch = {
158
+ ("hub", "login"): cmd_hub_login,
159
+ ("hub", "link"): cmd_hub_link,
160
+ ("hub", "sync"): cmd_hub_sync,
161
+ ("intent", "create"): cmd_intent_create,
162
+ ("intent", "list"): cmd_intent_list,
163
+ ("intent", "show"): cmd_intent_show,
164
+ ("intent", "activate"): cmd_intent_activate,
165
+ ("intent", "suspend"): cmd_intent_suspend,
166
+ ("intent", "done"): cmd_intent_done,
167
+ ("snap", "create"): cmd_snap_create,
168
+ ("snap", "list"): cmd_snap_list,
169
+ ("snap", "show"): cmd_snap_show,
170
+ ("snap", "feedback"): cmd_snap_feedback,
171
+ ("snap", "revert"): cmd_snap_revert,
172
+ ("decision", "create"): cmd_decision_create,
173
+ ("decision", "list"): cmd_decision_list,
174
+ ("decision", "show"): cmd_decision_show,
175
+ ("decision", "deprecate"): cmd_decision_deprecate,
176
+ ("decision", "attach"): cmd_decision_attach,
177
+ }
178
+ dispatch[(args.command, args.sub)](args)
@@ -0,0 +1 @@
1
+ """Command handlers for the Intent CLI."""
@@ -0,0 +1,41 @@
1
+ """Shared helpers for CLI command handlers."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from intent_cli.output import error
6
+ from intent_cli.store import VALID_STATUSES, ensure_init, git_root
7
+
8
+
9
+ def now_utc():
10
+ return datetime.now(timezone.utc).isoformat()
11
+
12
+
13
+ def require_init():
14
+ """Return .intent/ base path, or exit with a structured error."""
15
+ base = ensure_init()
16
+ if base is not None:
17
+ return base
18
+ if git_root() is None:
19
+ error(
20
+ "GIT_STATE_INVALID",
21
+ "Not inside a Git repository.",
22
+ suggested_fix="cd into a git repo and run: itt init",
23
+ )
24
+ error(
25
+ "NOT_INITIALIZED",
26
+ ".intent/ directory not found.",
27
+ suggested_fix="itt init",
28
+ )
29
+
30
+
31
+ def validate_status_filter(object_type, status):
32
+ """Validate a --status filter against the object's state machine."""
33
+ if status is None:
34
+ return
35
+ allowed = sorted(VALID_STATUSES[object_type])
36
+ if status not in allowed:
37
+ error(
38
+ "INVALID_INPUT",
39
+ f"Invalid status '{status}' for {object_type}. Allowed values: {', '.join(allowed)}.",
40
+ suggested_fix=f"Use one of: {', '.join(allowed)}",
41
+ )
@@ -0,0 +1,376 @@
1
+ """Core object command handlers for the Intent CLI."""
2
+
3
+ import json
4
+
5
+ from intent_cli import __version__
6
+ from intent_cli.commands.common import now_utc, require_init, validate_status_filter
7
+ from intent_cli.output import error, success
8
+ from intent_cli.store import (
9
+ VALID_STATUSES,
10
+ git_root,
11
+ init_workspace,
12
+ list_objects,
13
+ next_id,
14
+ read_config,
15
+ read_object,
16
+ validate_graph,
17
+ write_object,
18
+ )
19
+
20
+
21
+ def cmd_version(_args):
22
+ success("version", {"version": __version__})
23
+
24
+
25
+ def cmd_init(_args):
26
+ path, err = init_workspace()
27
+ if err == "GIT_STATE_INVALID":
28
+ error(
29
+ "GIT_STATE_INVALID",
30
+ "Not inside a Git repository.",
31
+ suggested_fix="cd into a git repo and run: itt init",
32
+ )
33
+ if err == "ALREADY_EXISTS":
34
+ error(
35
+ "ALREADY_EXISTS",
36
+ ".intent/ already exists.",
37
+ suggested_fix="Remove .intent/ first if you want to reinitialize.",
38
+ )
39
+ success("init", {"path": str(path)})
40
+
41
+
42
+ def cmd_inspect(_args):
43
+ base = require_init()
44
+ config = read_config(base)
45
+
46
+ active_intents = []
47
+ suspend_intents = []
48
+ for obj in list_objects(base, "intent"):
49
+ entry = {
50
+ "id": obj["id"],
51
+ "title": obj["title"],
52
+ "status": obj["status"],
53
+ "decision_ids": obj.get("decision_ids", []),
54
+ "latest_snap_id": obj["snap_ids"][-1] if obj.get("snap_ids") else None,
55
+ }
56
+ if obj["status"] == "active":
57
+ active_intents.append(entry)
58
+ elif obj["status"] == "suspend":
59
+ suspend_intents.append(entry)
60
+
61
+ active_decisions = []
62
+ for obj in list_objects(base, "decision", status="active"):
63
+ active_decisions.append({
64
+ "id": obj["id"],
65
+ "title": obj["title"],
66
+ "status": obj["status"],
67
+ "intent_ids": obj.get("intent_ids", []),
68
+ })
69
+
70
+ all_snaps = list_objects(base, "snap")
71
+ all_snaps.sort(key=lambda s: s.get("created_at", ""), reverse=True)
72
+ recent_snaps = []
73
+ for snap in all_snaps[:10]:
74
+ recent_snaps.append({
75
+ "id": snap["id"],
76
+ "title": snap["title"],
77
+ "intent_id": snap["intent_id"],
78
+ "status": snap["status"],
79
+ "summary": snap.get("summary", ""),
80
+ "feedback": snap.get("feedback", ""),
81
+ })
82
+
83
+ warnings = []
84
+ intent_ids_on_disk = {obj["id"] for obj in list_objects(base, "intent")}
85
+ for snap in all_snaps:
86
+ if snap.get("intent_id") and snap["intent_id"] not in intent_ids_on_disk:
87
+ warnings.append(f"Orphan snap {snap['id']}: intent {snap['intent_id']} not found")
88
+
89
+ print(json.dumps({
90
+ "ok": True,
91
+ "schema_version": config.get("schema_version", "1.0"),
92
+ "active_intents": active_intents,
93
+ "suspend_intents": suspend_intents,
94
+ "active_decisions": active_decisions,
95
+ "recent_snaps": recent_snaps,
96
+ "warnings": warnings,
97
+ }, indent=2, ensure_ascii=False))
98
+
99
+
100
+ def cmd_doctor(_args):
101
+ base = require_init()
102
+ success("doctor", validate_graph(base))
103
+
104
+
105
+ def cmd_intent_create(args):
106
+ base = require_init()
107
+ obj_id = next_id(base, "intent")
108
+
109
+ active_decisions = list_objects(base, "decision", status="active")
110
+ decision_ids = [decision["id"] for decision in active_decisions]
111
+
112
+ warnings = []
113
+ if not decision_ids:
114
+ warnings.append("No active decisions to attach.")
115
+
116
+ intent = {
117
+ "id": obj_id,
118
+ "object": "intent",
119
+ "created_at": now_utc(),
120
+ "title": args.title,
121
+ "status": "active",
122
+ "source_query": args.query,
123
+ "rationale": args.rationale,
124
+ "decision_ids": decision_ids,
125
+ "snap_ids": [],
126
+ }
127
+ write_object(base, "intent", obj_id, intent)
128
+
129
+ for decision in active_decisions:
130
+ if obj_id not in decision.get("intent_ids", []):
131
+ decision.setdefault("intent_ids", []).append(obj_id)
132
+ write_object(base, "decision", decision["id"], decision)
133
+
134
+ success("intent.create", intent, warnings)
135
+
136
+
137
+ def cmd_intent_list(args):
138
+ base = require_init()
139
+ validate_status_filter("intent", args.status)
140
+ objects = list_objects(base, "intent", status=args.status)
141
+ if args.decision:
142
+ objects = [obj for obj in objects if args.decision in obj.get("decision_ids", [])]
143
+ success("intent.list", objects)
144
+
145
+
146
+ def cmd_intent_show(args):
147
+ base = require_init()
148
+ obj = read_object(base, "intent", args.id)
149
+ if obj is None:
150
+ error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
151
+ success("intent.show", obj)
152
+
153
+
154
+ def cmd_intent_activate(args):
155
+ base = require_init()
156
+ obj = read_object(base, "intent", args.id)
157
+ if obj is None:
158
+ error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
159
+ if obj["status"] != "suspend":
160
+ error(
161
+ "STATE_CONFLICT",
162
+ f"Cannot activate intent with status '{obj['status']}'. Only 'suspend' intents can be activated.",
163
+ suggested_fix=f"itt intent show {args.id}",
164
+ )
165
+
166
+ obj["status"] = "active"
167
+
168
+ active_decisions = list_objects(base, "decision", status="active")
169
+ for decision in active_decisions:
170
+ if decision["id"] not in obj["decision_ids"]:
171
+ obj["decision_ids"].append(decision["id"])
172
+ if args.id not in decision.get("intent_ids", []):
173
+ decision.setdefault("intent_ids", []).append(args.id)
174
+ write_object(base, "decision", decision["id"], decision)
175
+
176
+ write_object(base, "intent", args.id, obj)
177
+ success("intent.activate", obj)
178
+
179
+
180
+ def cmd_intent_suspend(args):
181
+ base = require_init()
182
+ obj = read_object(base, "intent", args.id)
183
+ if obj is None:
184
+ error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
185
+ if obj["status"] != "active":
186
+ error(
187
+ "STATE_CONFLICT",
188
+ f"Cannot suspend intent with status '{obj['status']}'. Only 'active' intents can be suspended.",
189
+ suggested_fix=f"itt intent show {args.id}",
190
+ )
191
+
192
+ obj["status"] = "suspend"
193
+ write_object(base, "intent", args.id, obj)
194
+ success("intent.suspend", obj)
195
+
196
+
197
+ def cmd_intent_done(args):
198
+ base = require_init()
199
+ obj = read_object(base, "intent", args.id)
200
+ if obj is None:
201
+ error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
202
+ if obj["status"] != "active":
203
+ error(
204
+ "STATE_CONFLICT",
205
+ f"Cannot mark intent as done with status '{obj['status']}'. Only 'active' intents can be marked done.",
206
+ suggested_fix=f"itt intent show {args.id}",
207
+ )
208
+
209
+ obj["status"] = "done"
210
+ write_object(base, "intent", args.id, obj)
211
+ success("intent.done", obj)
212
+
213
+
214
+ def cmd_snap_create(args):
215
+ base = require_init()
216
+ intent_id = args.intent
217
+
218
+ intent = read_object(base, "intent", intent_id)
219
+ if intent is None:
220
+ error("OBJECT_NOT_FOUND", f"Intent {intent_id} not found.")
221
+ if intent["status"] != "active":
222
+ error(
223
+ "STATE_CONFLICT",
224
+ f"Cannot add snap to intent with status '{intent['status']}'. Only 'active' intents accept new snaps.",
225
+ suggested_fix=f"itt intent activate {intent_id}",
226
+ )
227
+
228
+ obj_id = next_id(base, "snap")
229
+ snap = {
230
+ "id": obj_id,
231
+ "object": "snap",
232
+ "created_at": now_utc(),
233
+ "title": args.title,
234
+ "status": "active",
235
+ "intent_id": intent_id,
236
+ "query": args.query,
237
+ "rationale": args.rationale,
238
+ "summary": args.summary,
239
+ "feedback": args.feedback,
240
+ }
241
+ write_object(base, "snap", obj_id, snap)
242
+
243
+ intent.setdefault("snap_ids", []).append(obj_id)
244
+ write_object(base, "intent", intent_id, intent)
245
+
246
+ success("snap.create", snap)
247
+
248
+
249
+ def cmd_snap_list(args):
250
+ base = require_init()
251
+ validate_status_filter("snap", args.status)
252
+ objects = list_objects(base, "snap", status=args.status)
253
+ if args.intent:
254
+ objects = [snap for snap in objects if snap.get("intent_id") == args.intent]
255
+ success("snap.list", objects)
256
+
257
+
258
+ def cmd_snap_show(args):
259
+ base = require_init()
260
+ obj = read_object(base, "snap", args.id)
261
+ if obj is None:
262
+ error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
263
+ success("snap.show", obj)
264
+
265
+
266
+ def cmd_snap_feedback(args):
267
+ base = require_init()
268
+ obj = read_object(base, "snap", args.id)
269
+ if obj is None:
270
+ error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
271
+ obj["feedback"] = args.feedback
272
+ write_object(base, "snap", args.id, obj)
273
+ success("snap.feedback", obj)
274
+
275
+
276
+ def cmd_snap_revert(args):
277
+ base = require_init()
278
+ obj = read_object(base, "snap", args.id)
279
+ if obj is None:
280
+ error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
281
+ if obj["status"] != "active":
282
+ error(
283
+ "STATE_CONFLICT",
284
+ f"Cannot revert snap with status '{obj['status']}'. Only 'active' snaps can be reverted.",
285
+ suggested_fix=f"itt snap show {args.id}",
286
+ )
287
+
288
+ obj["status"] = "reverted"
289
+ write_object(base, "snap", args.id, obj)
290
+ success("snap.revert", obj)
291
+
292
+
293
+ def cmd_decision_create(args):
294
+ base = require_init()
295
+ obj_id = next_id(base, "decision")
296
+
297
+ active_intents = list_objects(base, "intent", status="active")
298
+ intent_ids = [intent["id"] for intent in active_intents]
299
+
300
+ warnings = []
301
+ if not intent_ids:
302
+ warnings.append("No active intents to attach.")
303
+
304
+ decision = {
305
+ "id": obj_id,
306
+ "object": "decision",
307
+ "created_at": now_utc(),
308
+ "title": args.title,
309
+ "status": "active",
310
+ "rationale": args.rationale,
311
+ "intent_ids": intent_ids,
312
+ }
313
+ write_object(base, "decision", obj_id, decision)
314
+
315
+ for intent in active_intents:
316
+ if obj_id not in intent.get("decision_ids", []):
317
+ intent.setdefault("decision_ids", []).append(obj_id)
318
+ write_object(base, "intent", intent["id"], intent)
319
+
320
+ success("decision.create", decision, warnings)
321
+
322
+
323
+ def cmd_decision_list(args):
324
+ base = require_init()
325
+ validate_status_filter("decision", args.status)
326
+ objects = list_objects(base, "decision", status=args.status)
327
+ if args.intent:
328
+ objects = [obj for obj in objects if args.intent in obj.get("intent_ids", [])]
329
+ success("decision.list", objects)
330
+
331
+
332
+ def cmd_decision_show(args):
333
+ base = require_init()
334
+ obj = read_object(base, "decision", args.id)
335
+ if obj is None:
336
+ error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
337
+ success("decision.show", obj)
338
+
339
+
340
+ def cmd_decision_deprecate(args):
341
+ base = require_init()
342
+ obj = read_object(base, "decision", args.id)
343
+ if obj is None:
344
+ error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
345
+ if obj["status"] != "active":
346
+ error(
347
+ "STATE_CONFLICT",
348
+ f"Cannot deprecate decision with status '{obj['status']}'. Only 'active' decisions can be deprecated.",
349
+ suggested_fix=f"itt decision show {args.id}",
350
+ )
351
+
352
+ obj["status"] = "deprecated"
353
+ write_object(base, "decision", args.id, obj)
354
+ success("decision.deprecate", obj)
355
+
356
+
357
+ def cmd_decision_attach(args):
358
+ base = require_init()
359
+ decision = read_object(base, "decision", args.id)
360
+ if decision is None:
361
+ error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
362
+
363
+ intent_id = args.intent
364
+ intent = read_object(base, "intent", intent_id)
365
+ if intent is None:
366
+ error("OBJECT_NOT_FOUND", f"Intent {intent_id} not found.")
367
+
368
+ if intent_id not in decision.get("intent_ids", []):
369
+ decision.setdefault("intent_ids", []).append(intent_id)
370
+ write_object(base, "decision", args.id, decision)
371
+
372
+ if args.id not in intent.get("decision_ids", []):
373
+ intent.setdefault("decision_ids", []).append(args.id)
374
+ write_object(base, "intent", intent_id, intent)
375
+
376
+ success("decision.attach", decision)