-
Notifications
You must be signed in to change notification settings - Fork 3
v0.1.29 feat: intent capture via set_intent MCP tool #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c8ff7f5
7201aea
cda15cb
fc4a961
7712296
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -198,6 +198,41 @@ def read_events_per_file( | |||||||||
| return {fp: ev for fp, (_, ev) in best.items()} | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def read_intent_events( | ||||||||||
| path: str, | ||||||||||
| min_ts: int, | ||||||||||
| ) -> List[dict]: | ||||||||||
| """Read intent events from session.jsonl (type=intent, written by set_intent MCP tool). | ||||||||||
|
|
||||||||||
| Returns all matching intent events sorted by timestamp (most recent last). | ||||||||||
| """ | ||||||||||
| if not os.path.exists(path): | ||||||||||
| return [] | ||||||||||
| results = [] | ||||||||||
| try: | ||||||||||
| with open(path, "r", encoding="utf-8") as f: | ||||||||||
| for raw in f: | ||||||||||
| line = raw.strip() | ||||||||||
| if not line: | ||||||||||
| continue | ||||||||||
| try: | ||||||||||
| event = json.loads(line) | ||||||||||
| except Exception: | ||||||||||
| continue | ||||||||||
| if not isinstance(event, dict): | ||||||||||
| continue | ||||||||||
| if event.get("type") != "intent": | ||||||||||
| continue | ||||||||||
| event_ts = parse_event_ts(str(event.get("timestamp") or "")) | ||||||||||
| if event_ts < min_ts: | ||||||||||
| continue | ||||||||||
| results.append(event) | ||||||||||
| except Exception: | ||||||||||
| pass | ||||||||||
| results.sort(key=lambda e: parse_event_ts(str(e.get("timestamp") or ""))) | ||||||||||
| return results | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def dominant_event(events_by_file: Dict[str, dict], lines_by_file: Dict[str, List[Tuple[int, int]]]) -> dict: | ||||||||||
| """Pick the agent/model to use as the top-level record field. | ||||||||||
|
|
||||||||||
|
|
@@ -276,13 +311,17 @@ def main() -> int: | |||||||||
| if not isinstance(pending, dict): | ||||||||||
| pending = {} | ||||||||||
|
|
||||||||||
| min_ts = head_commit_ts(repo_root) | ||||||||||
| events_by_file = read_events_per_file( | ||||||||||
| session_log, | ||||||||||
| files_touched, | ||||||||||
| head_commit_ts(repo_root), | ||||||||||
| min_ts, | ||||||||||
| lines_by_file, | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| # Read agent-stated intent events from session.jsonl (written by set_intent MCP tool) | ||||||||||
| intent_events = read_intent_events(session_log, min_ts) | ||||||||||
|
|
||||||||||
| # Top-level agent/model/session come from the dominant event (most lines written) | ||||||||||
| event = dominant_event(events_by_file, lines_by_file) or {} | ||||||||||
|
|
||||||||||
|
|
@@ -307,9 +346,31 @@ def main() -> int: | |||||||||
| flags = [] | ||||||||||
| flags = [str(f) for f in flags] | ||||||||||
|
|
||||||||||
| intent = pending.get("intent") | ||||||||||
| if intent is not None: | ||||||||||
| intent = str(intent) | ||||||||||
| # Intent priority: agent-stated intent event > pending context > fallback | ||||||||||
| intent = None | ||||||||||
| intent_type = None | ||||||||||
| if intent_events: | ||||||||||
| # Use the most recent intent event (prefer matching session, else latest) | ||||||||||
| best_intent = None | ||||||||||
| if session_id and session_id != "unknown": | ||||||||||
| for ie in reversed(intent_events): | ||||||||||
| if str(ie.get("session_id") or "") == session_id: | ||||||||||
| best_intent = ie | ||||||||||
| break | ||||||||||
| if not best_intent: | ||||||||||
| best_intent = intent_events[-1] | ||||||||||
|
Comment on lines
+360
to
+361
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| intent = str(best_intent.get("description") or "").strip() or None | ||||||||||
| intent_type = str(best_intent.get("intent_type") or "").strip() or None | ||||||||||
|
|
||||||||||
| if not intent: | ||||||||||
| raw = pending.get("intent") | ||||||||||
| if raw is not None: | ||||||||||
| intent = str(raw).strip() or None | ||||||||||
|
|
||||||||||
| if not intent_type: | ||||||||||
| raw = pending.get("intent_type") | ||||||||||
| if raw is not None: | ||||||||||
| intent_type = str(raw).strip() or None | ||||||||||
|
|
||||||||||
| # Per-file attribution — each file maps to the agent/model that wrote it. | ||||||||||
| # Files with a session event that matches the dominant agent are omitted (finalize | ||||||||||
|
|
@@ -364,6 +425,8 @@ def main() -> int: | |||||||||
| payload["attribution"] = attribution | ||||||||||
| if intent: | ||||||||||
| payload["intent"] = intent | ||||||||||
| if intent_type: | ||||||||||
| payload["intent_type"] = intent_type | ||||||||||
| if trust is not None: | ||||||||||
| payload["trust"] = trust | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,9 @@ def main() -> int: | |
| parser.add_argument("--prompt", default="") | ||
| parser.add_argument("--files-read", default="") | ||
| parser.add_argument("--intent", default="") | ||
| parser.add_argument("--intent-type", default="", | ||
| choices=["bugfix","feature","refactor","test","docs", | ||
| "security","performance","config","dependency",""]) | ||
| parser.add_argument("--trust", type=int, default=None) | ||
| parser.add_argument("--flags", default="") | ||
| args = parser.parse_args() | ||
|
|
@@ -90,6 +93,7 @@ def main() -> int: | |
| "prompt": args.prompt or str(payload.get("prompt") or ""), | ||
| "files_read": parse_json_array(args.files_read) or payload.get("files_read") or [], | ||
| "intent": args.intent or str(payload.get("intent") or ""), | ||
| "intent_type": args.intent_type or str(payload.get("intent_type") or ""), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "flags": parse_json_array(args.flags) or payload.get("flags") or [], | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type=intentline insession.jsonl, not just the intent that was attached to the commit being finalized. If an agent records the next commit's intent while this post-commit hook is still running, that new event is deleted before the next pre-commit can read it, so the next trace silently loses its intent.