inscript 0.5.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -7,4 +7,5 @@ inscript.egg-info/dependency_links.txt
7
7
  inscript.egg-info/entry_points.txt
8
8
  inscript.egg-info/top_level.txt
9
9
  inscript_pkg/__init__.py
10
- inscript_pkg/hook.py
10
+ inscript_pkg/hook.py
11
+ inscript_pkg/reflect.py
@@ -17,7 +17,7 @@ import time
17
17
  from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
 
20
- __version__ = "0.5.0"
20
+ __version__ = "0.7.0"
21
21
 
22
22
  INSCRIPT_DIR = Path.home() / ".inscript"
23
23
  ACTIVE_PROJECT_FILE = INSCRIPT_DIR / "active_project"
@@ -0,0 +1,377 @@
1
+ """inscript reflect — identify what your codebase is becoming.
2
+
3
+ Reads inscript session history (diffs, prompts, touches) and optionally
4
+ tamachi structural analysis, feeds them to Claude, and returns:
5
+ 1. What archetype the codebase is converging toward
6
+ 2. The trajectory from recent diffs
7
+ 3. Predicted pain points based on archetype + trajectory
8
+ 4. What the codebase could become
9
+
10
+ Based on research from IntentDiff v4:
11
+ - Structure carries intent (architecture IS the domain)
12
+ - Invariants accumulate and converge toward known archetypes
13
+ - Bugfixes reveal hidden invariants
14
+ - Early commits predict the end state
15
+
16
+ Usage:
17
+ inscript reflect # reflect on current project
18
+ inscript reflect --sessions 5 # use last 5 sessions
19
+ inscript reflect --deep # include tamachi structural analysis
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ from . import (
29
+ INSCRIPT_DIR,
30
+ SESSIONS_DIR,
31
+ active_project,
32
+ active_session,
33
+ list_sessions,
34
+ session_dir,
35
+ )
36
+
37
+
38
+ # The 10 function archetypes (from IntentDiff v4 research)
39
+ ARCHETYPES = {
40
+ "coordinator": "Orchestrates other calls, manages control flow, dispatches work",
41
+ "handler": "Handles events, requests, callbacks — reacts to external input",
42
+ "factory": "Creates instances, builds objects, spawns processes",
43
+ "accessor": "Returns state, reads fields, property getters",
44
+ "transformer": "Pure function, transforms input to output",
45
+ "validator": "Returns bool, checks conditions, enforces constraints",
46
+ "state_mutator": "Modifies self state, writes fields",
47
+ "serializer": "Format conversion (JSON, dict, etc.)",
48
+ "initializer": "__init__ and setup methods",
49
+ "test": "Test functions",
50
+ }
51
+
52
+ # Known archetype convergence patterns (from invariant_convergence.md)
53
+ ARCHETYPE_PAIN_POINTS = {
54
+ "coordinator": [
55
+ "No graceful degradation — when one subsystem fails, the coordinator has no fallback",
56
+ "State management — coordinating multiple actors creates implicit shared state",
57
+ "The coordinator becomes a god object — too many responsibilities in one place",
58
+ "Missing health checks — no way to know if delegated work is actually progressing",
59
+ "Backpressure — no mechanism to slow down when workers are overloaded",
60
+ ],
61
+ "handler": [
62
+ "Handler explosion — too many handlers with overlapping concerns",
63
+ "Missing middleware — cross-cutting logic (auth, logging) duplicated across handlers",
64
+ "No error propagation strategy — handlers swallow errors silently",
65
+ "Missing validation at boundaries — handlers trust input too much",
66
+ ],
67
+ "factory": [
68
+ "Factory sprawl — too many factory methods creating slight variations",
69
+ "Missing cleanup — factories create but nothing tracks lifecycle or disposal",
70
+ "Configuration explosion — factories need more and more parameters",
71
+ ],
72
+ "state_mutator": [
73
+ "Mutation without notification — state changes don't trigger dependent updates",
74
+ "Missing invariant enforcement — state can reach invalid combinations",
75
+ "Concurrency hazards — multiple writers with no synchronization",
76
+ "No undo/rollback capability",
77
+ ],
78
+ "event_driven": [
79
+ "Event ordering — no guarantee events arrive in the right order",
80
+ "Missing dead letter queue — failed events are lost",
81
+ "Event schema evolution — changing event shapes breaks consumers",
82
+ "Debugging is hard — tracing causality through async event chains",
83
+ ],
84
+ "pipeline": [
85
+ "Pipeline stage coupling — stages know too much about each other",
86
+ "No partial failure handling — one stage fails, entire pipeline stalls",
87
+ "Observability gaps — hard to know where data is in the pipeline",
88
+ "Backpressure propagation — slow stages cause memory buildup",
89
+ ],
90
+ }
91
+
92
+
93
+ def _gather_session_data(project_path: str | None, max_sessions: int = 10) -> dict:
94
+ """Gather recent session data for a project."""
95
+ all_sessions = list_sessions()
96
+
97
+ # Filter to project if specified
98
+ if project_path:
99
+ all_sessions = [s for s in all_sessions if s.get("project") == project_path]
100
+
101
+ # Take most recent N
102
+ sessions = all_sessions[:max_sessions]
103
+
104
+ result = {
105
+ "session_count": len(sessions),
106
+ "prompts": [],
107
+ "diffs": [],
108
+ "files_touched": {},
109
+ "total_edits": 0,
110
+ "total_tokens": 0,
111
+ }
112
+
113
+ for s in sessions:
114
+ sid = s.get("session_id", "")
115
+ sdir = session_dir(sid)
116
+
117
+ # Load prompts
118
+ prompts_file = sdir / "prompts.jsonl"
119
+ if prompts_file.exists():
120
+ for line in prompts_file.open():
121
+ try:
122
+ p = json.loads(line)
123
+ p["session"] = sid[:8]
124
+ result["prompts"].append(p)
125
+ except json.JSONDecodeError:
126
+ pass
127
+
128
+ # Load diffs (just file + type, not full content for context window)
129
+ diffs_file = sdir / "diffs.jsonl"
130
+ if diffs_file.exists():
131
+ for line in diffs_file.open():
132
+ try:
133
+ d = json.loads(line)
134
+ result["diffs"].append({
135
+ "file": d.get("file", ""),
136
+ "tool": d.get("tool", ""),
137
+ "session": sid[:8],
138
+ "has_old_new": "old_string" in d,
139
+ "is_new_file": d.get("is_new", False),
140
+ })
141
+ except json.JSONDecodeError:
142
+ pass
143
+
144
+ # Load touches for file frequency
145
+ touches_file = sdir / "touches.jsonl"
146
+ if touches_file.exists():
147
+ for line in touches_file.open():
148
+ try:
149
+ t = json.loads(line)
150
+ f = t.get("file", "")
151
+ action = t.get("action", "")
152
+ if f:
153
+ if f not in result["files_touched"]:
154
+ result["files_touched"][f] = {"reads": 0, "edits": 0}
155
+ if action == "read":
156
+ result["files_touched"][f]["reads"] += 1
157
+ elif action in ("edit", "write"):
158
+ result["files_touched"][f]["edits"] += 1
159
+ result["total_edits"] += 1
160
+ except json.JSONDecodeError:
161
+ pass
162
+
163
+ # Token usage from summary
164
+ summary_file = sdir / "summary.json"
165
+ if summary_file.exists():
166
+ try:
167
+ summary = json.loads(summary_file.read_text())
168
+ tokens = summary.get("tokens", {})
169
+ result["total_tokens"] += tokens.get("total_tokens", 0)
170
+ except (json.JSONDecodeError, OSError):
171
+ pass
172
+
173
+ return result
174
+
175
+
176
+ def _load_tamachi_analysis(project_path: str) -> dict | None:
177
+ """Try to load tamachi structural analysis for the project."""
178
+ project = Path(project_path)
179
+
180
+ # Check .tamachi/ directory
181
+ for candidate in [
182
+ project / ".tamachi" / "tamachi.json",
183
+ project / ".tamachi",
184
+ ]:
185
+ if candidate.is_file():
186
+ try:
187
+ data = json.loads(candidate.read_text())
188
+ tami = data.get("tami", data)
189
+ return {
190
+ "classes": len(tami.get("classes", [])),
191
+ "mutations": len(tami.get("mutation_sites", [])),
192
+ "reads": len(tami.get("method_read_sites", [])),
193
+ "state_machines": [
194
+ {"class": sm.get("class_name"), "field": sm.get("field_name"),
195
+ "states": sm.get("states", [])}
196
+ for sm in tami.get("state_machines", [])
197
+ ],
198
+ "boolean_lifecycles": len(tami.get("boolean_lifecycle_invariants", [])),
199
+ "writer_set_groups": len(tami.get("writer_set_groups", [])),
200
+ "behavioral_clusters": len(tami.get("behavioral_clusters", [])),
201
+ }
202
+ except (json.JSONDecodeError, OSError):
203
+ pass
204
+ elif candidate.is_dir():
205
+ for f in candidate.glob("*_analysis.json"):
206
+ try:
207
+ data = json.loads(f.read_text())
208
+ tami = data.get("tami", data)
209
+ return {
210
+ "classes": len(tami.get("classes", [])),
211
+ "mutations": len(tami.get("mutation_sites", [])),
212
+ "reads": len(tami.get("method_read_sites", [])),
213
+ "state_machines": [
214
+ {"class": sm.get("class_name"), "field": sm.get("field_name"),
215
+ "states": sm.get("states", [])}
216
+ for sm in tami.get("state_machines", [])
217
+ ],
218
+ "boolean_lifecycles": len(tami.get("boolean_lifecycle_invariants", [])),
219
+ "writer_set_groups": len(tami.get("writer_set_groups", [])),
220
+ "behavioral_clusters": len(tami.get("behavioral_clusters", [])),
221
+ }
222
+ except (json.JSONDecodeError, OSError):
223
+ pass
224
+
225
+ return None
226
+
227
+
228
+ def _build_reflect_prompt(session_data: dict, tamachi_data: dict | None, project_name: str) -> str:
229
+ """Build the LLM prompt for reflection."""
230
+
231
+ # Top edited files
232
+ top_files = sorted(
233
+ session_data["files_touched"].items(),
234
+ key=lambda x: x[1]["edits"],
235
+ reverse=True,
236
+ )[:15]
237
+
238
+ # Recent prompts (last 20)
239
+ recent_prompts = session_data["prompts"][-20:]
240
+
241
+ # Diff summary
242
+ new_files = [d for d in session_data["diffs"] if d.get("is_new_file")]
243
+ edited_files = [d for d in session_data["diffs"] if d.get("has_old_new")]
244
+
245
+ prompt = f"""You are analyzing a codebase's development trajectory to identify what it's becoming.
246
+
247
+ ## Project: {project_name}
248
+
249
+ ## Recent Activity ({session_data['session_count']} sessions, {session_data['total_edits']} edits)
250
+
251
+ ### Most Edited Files
252
+ """
253
+ for f, counts in top_files:
254
+ prompt += f"- `{f}` — {counts['edits']} edits, {counts['reads']} reads\n"
255
+
256
+ prompt += f"\n### New Files Created ({len(new_files)})\n"
257
+ for d in new_files[:10]:
258
+ prompt += f"- `{d['file']}`\n"
259
+
260
+ prompt += f"\n### Recent Prompts (what the developer asked for)\n"
261
+ for p in recent_prompts:
262
+ tag = f" [{p['tag']}]" if p.get("tag") else ""
263
+ prompt += f"- \"{p.get('prompt', '')}\"{tag}\n"
264
+
265
+ if tamachi_data:
266
+ prompt += f"""
267
+ ### Structural Analysis (from tamachi)
268
+ - {tamachi_data['classes']} classes, {tamachi_data['mutations']} mutation sites, {tamachi_data['reads']} read sites
269
+ - {len(tamachi_data['state_machines'])} state machines: {', '.join(f"{sm['class']}.{sm['field']}" for sm in tamachi_data['state_machines'][:5])}
270
+ - {tamachi_data['boolean_lifecycles']} boolean lifecycle invariants
271
+ - {tamachi_data['writer_set_groups']} writer set groups
272
+ - {tamachi_data['behavioral_clusters']} behavioral clusters
273
+ """
274
+
275
+ prompt += f"""
276
+ ## Known Archetypes
277
+ These are the known system archetypes that codebases converge toward:
278
+ - **Coordinator/Supervisor**: Orchestrates workers, manages lifecycle, fault tolerance (Erlang OTP pattern)
279
+ - **Event-Driven Pipeline**: Events flow through handlers, async processing, pub/sub
280
+ - **Request-Response Server**: Handlers process requests, middleware chains, routing
281
+ - **Data Pipeline**: ETL, transformation stages, batch processing
282
+ - **State Machine Engine**: Manages entities through lifecycle states
283
+ - **Analysis Engine**: Reads data, computes insights, produces reports
284
+ - **CLI/Tool**: Command dispatch, argument parsing, output formatting
285
+
286
+ ## Your Task
287
+
288
+ Based on the development activity, structural analysis, and file patterns:
289
+
290
+ 1. **What it is**: Identify the primary archetype this codebase is converging toward. Name it precisely. Explain what structural patterns reveal this.
291
+
292
+ 2. **Trajectory**: What direction is the project moving based on recent prompts and edits? What's being built right now vs what was built before?
293
+
294
+ 3. **Predicted pain points**: Based on the archetype, what infrastructure problems will this project hit next? Be specific — name the files or patterns that will break. Reference the archetype's known failure modes.
295
+
296
+ 4. **What it could become**: Project the trajectory forward. If the current development direction continues, what will this system look like in 1 month? What architectural decisions are being implicitly made right now?
297
+
298
+ 5. **Hidden invariants**: What invariants exist in this codebase that aren't explicitly enforced? What "rules" does the code follow that a new contributor wouldn't know?
299
+
300
+ Respond in this format:
301
+
302
+ ## Archetype
303
+ [Name and explanation]
304
+
305
+ ## Trajectory
306
+ [Direction from recent activity]
307
+
308
+ ## Predicted Pain Points
309
+ 1. [Specific prediction with file/pattern reference]
310
+ 2. [...]
311
+ 3. [...]
312
+
313
+ ## What It Could Become
314
+ [1-month projection]
315
+
316
+ ## Hidden Invariants
317
+ [Rules the code follows implicitly]
318
+ """
319
+
320
+ return prompt
321
+
322
+
323
+ def reflect(max_sessions: int = 10, deep: bool = False) -> str:
324
+ """Run the reflect analysis. Returns the LLM's analysis as a string."""
325
+ import anthropic
326
+
327
+ project = active_project()
328
+ project_path = str(project) if project else None
329
+ project_name = project.name if project else "unknown"
330
+
331
+ # Gather data
332
+ session_data = _gather_session_data(project_path, max_sessions)
333
+
334
+ if session_data["session_count"] == 0:
335
+ return "No session data found. Use inscript for a few sessions first."
336
+
337
+ # Load tamachi if --deep or if it exists
338
+ tamachi_data = None
339
+ if project_path:
340
+ tamachi_data = _load_tamachi_analysis(project_path)
341
+
342
+ # Build prompt
343
+ prompt = _build_reflect_prompt(session_data, tamachi_data, project_name)
344
+
345
+ # Call Claude
346
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
347
+ if not api_key:
348
+ return "ANTHROPIC_API_KEY not set. inscript reflect needs Claude API access."
349
+
350
+ client = anthropic.Anthropic(api_key=api_key)
351
+
352
+ print(f"Reflecting on {project_name} ({session_data['session_count']} sessions, {session_data['total_edits']} edits)...\n", file=sys.stderr)
353
+
354
+ message = client.messages.create(
355
+ model="claude-sonnet-4-20250514",
356
+ max_tokens=4096,
357
+ messages=[{"role": "user", "content": prompt}],
358
+ )
359
+
360
+ return message.content[0].text
361
+
362
+
363
+ def main():
364
+ """CLI entry point for inscript reflect."""
365
+ import argparse
366
+
367
+ parser = argparse.ArgumentParser(description="Reflect on your codebase's trajectory")
368
+ parser.add_argument("--sessions", type=int, default=10, help="Number of recent sessions to analyze")
369
+ parser.add_argument("--deep", action="store_true", help="Include tamachi structural analysis")
370
+ args = parser.parse_args()
371
+
372
+ result = reflect(max_sessions=args.sessions, deep=args.deep)
373
+ print(result)
374
+
375
+
376
+ if __name__ == "__main__":
377
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript"
7
- version = "0.5.0"
7
+ version = "0.7.0"
8
8
  description = "Universal agent activity ledger. Records what AI agents do."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes