@pjmendonca/devflow 1.9.0

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 (124) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/devflow-checkpoint.js +10 -0
  5. package/bin/devflow-collab.js +10 -0
  6. package/bin/devflow-cost.js +10 -0
  7. package/bin/devflow-create-persona.js +10 -0
  8. package/bin/devflow-init.js +10 -0
  9. package/bin/devflow-memory.js +10 -0
  10. package/bin/devflow-new-doc.js +10 -0
  11. package/bin/devflow-personalize.js +10 -0
  12. package/bin/devflow-setup-checkpoint.js +10 -0
  13. package/bin/devflow-story.js +10 -0
  14. package/bin/devflow-tech-debt.js +10 -0
  15. package/bin/devflow-validate-overrides.js +10 -0
  16. package/bin/devflow-validate.js +10 -0
  17. package/bin/devflow-version.js +10 -0
  18. package/lib/constants.js +30 -0
  19. package/lib/exec-python.js +78 -0
  20. package/lib/python-check.js +178 -0
  21. package/package.json +64 -0
  22. package/tooling/.automation/agents/architect.md +135 -0
  23. package/tooling/.automation/agents/ba.md +70 -0
  24. package/tooling/.automation/agents/dev.md +79 -0
  25. package/tooling/.automation/agents/maintainer.md +97 -0
  26. package/tooling/.automation/agents/pm.md +116 -0
  27. package/tooling/.automation/agents/reviewer.md +141 -0
  28. package/tooling/.automation/agents/sm.md +61 -0
  29. package/tooling/.automation/agents/writer.md +193 -0
  30. package/tooling/.automation/config.ps1.template +61 -0
  31. package/tooling/.automation/config.sh.template +48 -0
  32. package/tooling/.automation/memory/.gitkeep +6 -0
  33. package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
  34. package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
  35. package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
  36. package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
  37. package/tooling/.automation/overrides/templates/README.md +113 -0
  38. package/tooling/.automation/overrides/templates/architect/README.md +27 -0
  39. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
  40. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
  41. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
  42. package/tooling/.automation/overrides/templates/ba/README.md +27 -0
  43. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
  44. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
  45. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
  46. package/tooling/.automation/overrides/templates/dev/README.md +32 -0
  47. package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
  48. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
  49. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
  50. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
  51. package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
  52. package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
  53. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
  54. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
  55. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
  56. package/tooling/.automation/overrides/templates/pm/README.md +27 -0
  57. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
  58. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
  59. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
  60. package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
  61. package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
  62. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
  63. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
  64. package/tooling/.automation/overrides/templates/sm/README.md +11 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
  67. package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
  68. package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
  69. package/tooling/.automation/overrides/templates/writer/README.md +27 -0
  70. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
  71. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
  72. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
  73. package/tooling/completions/DevflowCompletion.ps1 +213 -0
  74. package/tooling/completions/_run-story +116 -0
  75. package/tooling/completions/run-story-completion.bash +136 -0
  76. package/tooling/docs/DOC-STANDARD.md +717 -0
  77. package/tooling/docs/sprint-status.yaml.template +24 -0
  78. package/tooling/docs/templates/bug-report.md +234 -0
  79. package/tooling/docs/templates/migration-spec.md +274 -0
  80. package/tooling/docs/templates/refactor-spec.md +86 -0
  81. package/tooling/docs/templates/tech-debt.md +86 -0
  82. package/tooling/scripts/context_checkpoint.py +556 -0
  83. package/tooling/scripts/cost_dashboard.py +617 -0
  84. package/tooling/scripts/create-persona.py +690 -0
  85. package/tooling/scripts/create-persona.sh +435 -0
  86. package/tooling/scripts/init-project-workflow.ps1 +651 -0
  87. package/tooling/scripts/init-project-workflow.py +70 -0
  88. package/tooling/scripts/init-project-workflow.sh +746 -0
  89. package/tooling/scripts/lib/__init__.py +35 -0
  90. package/tooling/scripts/lib/agent_handoff.py +526 -0
  91. package/tooling/scripts/lib/agent_router.py +698 -0
  92. package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
  93. package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
  94. package/tooling/scripts/lib/claude-cli.ps1 +952 -0
  95. package/tooling/scripts/lib/claude-cli.sh +1293 -0
  96. package/tooling/scripts/lib/cost_config.py +222 -0
  97. package/tooling/scripts/lib/cost_display.py +443 -0
  98. package/tooling/scripts/lib/cost_tracker.py +710 -0
  99. package/tooling/scripts/lib/currency_converter.py +328 -0
  100. package/tooling/scripts/lib/errors.py +438 -0
  101. package/tooling/scripts/lib/override-loader.sh +286 -0
  102. package/tooling/scripts/lib/pair_programming.py +589 -0
  103. package/tooling/scripts/lib/shared_memory.py +637 -0
  104. package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
  105. package/tooling/scripts/memory_summarize.py +324 -0
  106. package/tooling/scripts/new-doc.ps1 +405 -0
  107. package/tooling/scripts/new-doc.py +93 -0
  108. package/tooling/scripts/new-doc.sh +534 -0
  109. package/tooling/scripts/personalize_agent.py +385 -0
  110. package/tooling/scripts/rollback-migration.sh +540 -0
  111. package/tooling/scripts/run-collab.ps1 +251 -0
  112. package/tooling/scripts/run-collab.py +605 -0
  113. package/tooling/scripts/run-collab.sh +110 -0
  114. package/tooling/scripts/run-story.ps1 +490 -0
  115. package/tooling/scripts/run-story.py +387 -0
  116. package/tooling/scripts/run-story.sh +467 -0
  117. package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
  118. package/tooling/scripts/setup-checkpoint-service.py +87 -0
  119. package/tooling/scripts/setup-checkpoint-service.sh +236 -0
  120. package/tooling/scripts/tech-debt-tracker.py +608 -0
  121. package/tooling/scripts/update_version.py +244 -0
  122. package/tooling/scripts/validate-overrides.py +511 -0
  123. package/tooling/scripts/validate-overrides.sh +432 -0
  124. package/tooling/scripts/validate_setup.py +539 -0
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Shared Memory System - Cross-Agent Knowledge Sharing
4
+
5
+ Provides a shared memory pool and knowledge graph that all agents can read/write.
6
+ Enables agents to share decisions, learnings, and context across the workflow.
7
+
8
+ Features:
9
+ - Shared memory pool (all agents can contribute)
10
+ - Knowledge graph with queryable decisions
11
+ - Automatic timestamping and attribution
12
+ - Memory search and retrieval
13
+ - Decision tracking with context
14
+
15
+ Usage:
16
+ from lib.shared_memory import SharedMemory, KnowledgeGraph
17
+
18
+ # Shared memory
19
+ memory = SharedMemory(story_key="3-5")
20
+ memory.add("DEV", "Decided to use PostgreSQL for user data", tags=["database", "decision"])
21
+ entries = memory.search("database")
22
+
23
+ # Knowledge graph
24
+ kg = KnowledgeGraph(story_key="3-5")
25
+ kg.add_decision("ARCHITECT", "auth-approach", "Use JWT with refresh tokens",
26
+ context={"reason": "Stateless, scalable"})
27
+ decision = kg.query("What authentication approach was decided?")
28
+ """
29
+
30
+ import json
31
+ import re
32
+ from dataclasses import asdict, dataclass, field
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+ from typing import Any, Optional
36
+
37
+ # Storage paths
38
+ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
39
+ MEMORY_DIR = PROJECT_ROOT / "tooling" / ".automation" / "memory"
40
+ SHARED_MEMORY_DIR = MEMORY_DIR / "shared"
41
+ KNOWLEDGE_GRAPH_DIR = MEMORY_DIR / "knowledge"
42
+
43
+
44
+ @dataclass
45
+ class MemoryEntry:
46
+ """A single shared memory entry."""
47
+
48
+ id: str
49
+ timestamp: str
50
+ agent: str
51
+ content: str
52
+ tags: list[str] = field(default_factory=list)
53
+ story_key: Optional[str] = None
54
+ references: list[str] = field(default_factory=list) # IDs of related entries
55
+
56
+ def to_dict(self) -> dict:
57
+ return asdict(self)
58
+
59
+ @classmethod
60
+ def from_dict(cls, data: dict) -> "MemoryEntry":
61
+ return cls(**data)
62
+
63
+ def matches_query(self, query: str) -> bool:
64
+ """Check if entry matches a search query."""
65
+ query_lower = query.lower()
66
+ return (
67
+ query_lower in self.content.lower()
68
+ or any(query_lower in tag.lower() for tag in self.tags)
69
+ or query_lower in self.agent.lower()
70
+ )
71
+
72
+
73
+ @dataclass
74
+ class Decision:
75
+ """A tracked decision in the knowledge graph."""
76
+
77
+ id: str
78
+ timestamp: str
79
+ agent: str
80
+ topic: str
81
+ decision: str
82
+ context: dict[str, Any] = field(default_factory=dict)
83
+ supersedes: Optional[str] = None # ID of decision this replaces
84
+ status: str = "active" # active, superseded, revoked
85
+
86
+ def to_dict(self) -> dict:
87
+ return asdict(self)
88
+
89
+ @classmethod
90
+ def from_dict(cls, data: dict) -> "Decision":
91
+ return cls(**data)
92
+
93
+
94
+ @dataclass
95
+ class HandoffSummary:
96
+ """Summary passed from one agent to another."""
97
+
98
+ id: str
99
+ timestamp: str
100
+ from_agent: str
101
+ to_agent: str
102
+ story_key: str
103
+ summary: str
104
+ key_decisions: list[str] = field(default_factory=list)
105
+ blockers_resolved: list[str] = field(default_factory=list)
106
+ watch_out_for: list[str] = field(default_factory=list)
107
+ files_touched: list[str] = field(default_factory=list)
108
+ next_steps: list[str] = field(default_factory=list)
109
+
110
+ def to_dict(self) -> dict:
111
+ return asdict(self)
112
+
113
+ @classmethod
114
+ def from_dict(cls, data: dict) -> "HandoffSummary":
115
+ return cls(**data)
116
+
117
+ def to_markdown(self) -> str:
118
+ """Convert handoff to markdown format."""
119
+ lines = [
120
+ f"## Handoff: {self.from_agent} → {self.to_agent}",
121
+ "",
122
+ f"**Story**: {self.story_key}",
123
+ f"**Time**: {self.timestamp}",
124
+ "",
125
+ "### Summary",
126
+ self.summary,
127
+ "",
128
+ ]
129
+
130
+ if self.key_decisions:
131
+ lines.append("### Key Decisions")
132
+ for decision in self.key_decisions:
133
+ lines.append(f"- {decision}")
134
+ lines.append("")
135
+
136
+ if self.blockers_resolved:
137
+ lines.append("### Blockers Resolved")
138
+ for blocker in self.blockers_resolved:
139
+ lines.append(f"- ✅ {blocker}")
140
+ lines.append("")
141
+
142
+ if self.watch_out_for:
143
+ lines.append("### ⚠️ Watch Out For")
144
+ for warning in self.watch_out_for:
145
+ lines.append(f"- {warning}")
146
+ lines.append("")
147
+
148
+ if self.files_touched:
149
+ lines.append("### Files Modified")
150
+ for file in self.files_touched:
151
+ lines.append(f"- `{file}`")
152
+ lines.append("")
153
+
154
+ if self.next_steps:
155
+ lines.append("### Next Steps")
156
+ for i, step in enumerate(self.next_steps, 1):
157
+ lines.append(f"{i}. {step}")
158
+ lines.append("")
159
+
160
+ return "\n".join(lines)
161
+
162
+
163
+ class SharedMemory:
164
+ """Cross-agent shared memory pool."""
165
+
166
+ def __init__(self, story_key: Optional[str] = None):
167
+ self.story_key = story_key
168
+ self.memory_dir = SHARED_MEMORY_DIR
169
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
170
+ self.entries: list[MemoryEntry] = []
171
+ self._load()
172
+
173
+ def _get_file_path(self) -> Path:
174
+ """Get the memory file path."""
175
+ if self.story_key:
176
+ return self.memory_dir / f"shared_{self.story_key}.json"
177
+ return self.memory_dir / "shared_global.json"
178
+
179
+ def _load(self):
180
+ """Load existing memory entries."""
181
+ file_path = self._get_file_path()
182
+ if file_path.exists():
183
+ try:
184
+ with open(file_path) as f:
185
+ data = json.load(f)
186
+ self.entries = [MemoryEntry.from_dict(e) for e in data.get("entries", [])]
187
+ except (json.JSONDecodeError, KeyError):
188
+ self.entries = []
189
+
190
+ def _save(self):
191
+ """Save memory entries to disk."""
192
+ file_path = self._get_file_path()
193
+ with open(file_path, "w") as f:
194
+ json.dump(
195
+ {
196
+ "story_key": self.story_key,
197
+ "last_updated": datetime.now().isoformat(),
198
+ "entries": [e.to_dict() for e in self.entries],
199
+ },
200
+ f,
201
+ indent=2,
202
+ )
203
+
204
+ def _generate_id(self) -> str:
205
+ """Generate a unique ID for an entry."""
206
+ import uuid
207
+
208
+ return f"mem_{uuid.uuid4().hex[:8]}"
209
+
210
+ def add(
211
+ self,
212
+ agent: str,
213
+ content: str,
214
+ tags: Optional[list[str]] = None,
215
+ references: Optional[list[str]] = None,
216
+ ) -> MemoryEntry:
217
+ """Add a new memory entry."""
218
+ entry = MemoryEntry(
219
+ id=self._generate_id(),
220
+ timestamp=datetime.now().isoformat(),
221
+ agent=agent,
222
+ content=content,
223
+ tags=tags or [],
224
+ story_key=self.story_key,
225
+ references=references or [],
226
+ )
227
+ self.entries.append(entry)
228
+ self._save()
229
+ return entry
230
+
231
+ def search(
232
+ self,
233
+ query: str,
234
+ agent: Optional[str] = None,
235
+ tags: Optional[list[str]] = None,
236
+ limit: int = 10,
237
+ ) -> list[MemoryEntry]:
238
+ """Search memory entries."""
239
+ results = []
240
+
241
+ for entry in reversed(self.entries): # Most recent first
242
+ if agent and entry.agent != agent:
243
+ continue
244
+ if tags and not any(t in entry.tags for t in tags):
245
+ continue
246
+ if query and not entry.matches_query(query):
247
+ continue
248
+ results.append(entry)
249
+ if len(results) >= limit:
250
+ break
251
+
252
+ return results
253
+
254
+ def get_by_agent(self, agent: str, limit: int = 20) -> list[MemoryEntry]:
255
+ """Get all entries from a specific agent."""
256
+ return [e for e in reversed(self.entries) if e.agent == agent][:limit]
257
+
258
+ def get_recent(self, limit: int = 20) -> list[MemoryEntry]:
259
+ """Get most recent entries."""
260
+ return list(reversed(self.entries))[:limit]
261
+
262
+ def get_by_tags(self, tags: list[str]) -> list[MemoryEntry]:
263
+ """Get entries matching any of the given tags."""
264
+ return [e for e in self.entries if any(t in e.tags for t in tags)]
265
+
266
+ def to_context_string(self, limit: int = 10) -> str:
267
+ """Convert recent memory to a context string for agents."""
268
+ recent = self.get_recent(limit)
269
+ if not recent:
270
+ return "No shared memories yet."
271
+
272
+ lines = ["## Shared Team Memory", ""]
273
+ for entry in recent:
274
+ tags_str = f" [{', '.join(entry.tags)}]" if entry.tags else ""
275
+ lines.append(f"- **{entry.agent}** ({entry.timestamp[:10]}){tags_str}: {entry.content}")
276
+
277
+ return "\n".join(lines)
278
+
279
+
280
+ class KnowledgeGraph:
281
+ """Queryable knowledge graph for agent decisions."""
282
+
283
+ def __init__(self, story_key: Optional[str] = None):
284
+ self.story_key = story_key
285
+ self.knowledge_dir = KNOWLEDGE_GRAPH_DIR
286
+ self.knowledge_dir.mkdir(parents=True, exist_ok=True)
287
+ self.decisions: dict[str, Decision] = {}
288
+ self.topic_index: dict[str, list[str]] = {} # topic -> decision IDs
289
+ self.handoffs: list[HandoffSummary] = []
290
+ self._load()
291
+
292
+ def _get_file_path(self) -> Path:
293
+ """Get the knowledge graph file path."""
294
+ if self.story_key:
295
+ return self.knowledge_dir / f"kg_{self.story_key}.json"
296
+ return self.knowledge_dir / "kg_global.json"
297
+
298
+ def _load(self):
299
+ """Load existing knowledge graph."""
300
+ file_path = self._get_file_path()
301
+ if file_path.exists():
302
+ try:
303
+ with open(file_path) as f:
304
+ data = json.load(f)
305
+ self.decisions = {
306
+ k: Decision.from_dict(v) for k, v in data.get("decisions", {}).items()
307
+ }
308
+ self.topic_index = data.get("topic_index", {})
309
+ self.handoffs = [HandoffSummary.from_dict(h) for h in data.get("handoffs", [])]
310
+ except (json.JSONDecodeError, KeyError):
311
+ pass
312
+
313
+ def _save(self):
314
+ """Save knowledge graph to disk."""
315
+ file_path = self._get_file_path()
316
+ with open(file_path, "w") as f:
317
+ json.dump(
318
+ {
319
+ "story_key": self.story_key,
320
+ "last_updated": datetime.now().isoformat(),
321
+ "decisions": {k: v.to_dict() for k, v in self.decisions.items()},
322
+ "topic_index": self.topic_index,
323
+ "handoffs": [h.to_dict() for h in self.handoffs],
324
+ },
325
+ f,
326
+ indent=2,
327
+ )
328
+
329
+ def _generate_id(self) -> str:
330
+ """Generate a unique decision ID."""
331
+ import uuid
332
+
333
+ return f"dec_{uuid.uuid4().hex[:8]}"
334
+
335
+ def add_decision(
336
+ self,
337
+ agent: str,
338
+ topic: str,
339
+ decision: str,
340
+ context: Optional[dict[str, Any]] = None,
341
+ supersedes: Optional[str] = None,
342
+ ) -> Decision:
343
+ """Record a decision in the knowledge graph."""
344
+ decision_id = self._generate_id()
345
+
346
+ # Mark superseded decision
347
+ if supersedes and supersedes in self.decisions:
348
+ self.decisions[supersedes].status = "superseded"
349
+
350
+ dec = Decision(
351
+ id=decision_id,
352
+ timestamp=datetime.now().isoformat(),
353
+ agent=agent,
354
+ topic=topic,
355
+ decision=decision,
356
+ context=context or {},
357
+ supersedes=supersedes,
358
+ status="active",
359
+ )
360
+
361
+ self.decisions[decision_id] = dec
362
+
363
+ # Update topic index
364
+ topic_key = topic.lower().replace(" ", "-")
365
+ if topic_key not in self.topic_index:
366
+ self.topic_index[topic_key] = []
367
+ self.topic_index[topic_key].append(decision_id)
368
+
369
+ self._save()
370
+ return dec
371
+
372
+ def query(self, question: str) -> Optional[dict[str, Any]]:
373
+ """Query the knowledge graph with a natural language question."""
374
+ question_lower = question.lower()
375
+
376
+ # Extract potential topics from the question
377
+ keywords = self._extract_keywords(question_lower)
378
+
379
+ # Search for matching decisions
380
+ matches = []
381
+ for dec in self.decisions.values():
382
+ if dec.status != "active":
383
+ continue
384
+
385
+ score = 0
386
+ dec_text = f"{dec.topic} {dec.decision}".lower()
387
+
388
+ for keyword in keywords:
389
+ if keyword in dec_text:
390
+ score += 1
391
+ if keyword in dec.topic.lower():
392
+ score += 2 # Topic matches are more relevant
393
+
394
+ if score > 0:
395
+ matches.append((score, dec))
396
+
397
+ if not matches:
398
+ return None
399
+
400
+ # Return best match
401
+ matches.sort(key=lambda x: x[0], reverse=True)
402
+ best = matches[0][1]
403
+
404
+ return {
405
+ "decision": best.decision,
406
+ "agent": best.agent,
407
+ "topic": best.topic,
408
+ "timestamp": best.timestamp,
409
+ "context": best.context,
410
+ "confidence": "high" if matches[0][0] > 2 else "medium",
411
+ }
412
+
413
+ def _extract_keywords(self, text: str) -> list[str]:
414
+ """Extract relevant keywords from text."""
415
+ # Remove common question words
416
+ stop_words = {
417
+ "what",
418
+ "which",
419
+ "who",
420
+ "how",
421
+ "why",
422
+ "when",
423
+ "where",
424
+ "is",
425
+ "are",
426
+ "was",
427
+ "were",
428
+ "did",
429
+ "does",
430
+ "do",
431
+ "the",
432
+ "a",
433
+ "an",
434
+ "decided",
435
+ "recommended",
436
+ "suggested",
437
+ "approach",
438
+ "about",
439
+ }
440
+
441
+ words = re.findall(r"\b\w+\b", text.lower())
442
+ return [w for w in words if w not in stop_words and len(w) > 2]
443
+
444
+ def get_decisions_by_agent(self, agent: str) -> list[Decision]:
445
+ """Get all active decisions by an agent."""
446
+ return [d for d in self.decisions.values() if d.agent == agent and d.status == "active"]
447
+
448
+ def get_decisions_by_topic(self, topic: str) -> list[Decision]:
449
+ """Get all decisions for a topic."""
450
+ topic_key = topic.lower().replace(" ", "-")
451
+ decision_ids = self.topic_index.get(topic_key, [])
452
+ return [
453
+ self.decisions[did]
454
+ for did in decision_ids
455
+ if did in self.decisions and self.decisions[did].status == "active"
456
+ ]
457
+
458
+ def add_handoff(
459
+ self,
460
+ from_agent: str,
461
+ to_agent: str,
462
+ story_key: str,
463
+ summary: str,
464
+ key_decisions: Optional[list[str]] = None,
465
+ blockers_resolved: Optional[list[str]] = None,
466
+ watch_out_for: Optional[list[str]] = None,
467
+ files_touched: Optional[list[str]] = None,
468
+ next_steps: Optional[list[str]] = None,
469
+ ) -> HandoffSummary:
470
+ """Create a handoff summary between agents."""
471
+ import uuid
472
+
473
+ handoff = HandoffSummary(
474
+ id=f"handoff_{uuid.uuid4().hex[:8]}",
475
+ timestamp=datetime.now().isoformat(),
476
+ from_agent=from_agent,
477
+ to_agent=to_agent,
478
+ story_key=story_key,
479
+ summary=summary,
480
+ key_decisions=key_decisions or [],
481
+ blockers_resolved=blockers_resolved or [],
482
+ watch_out_for=watch_out_for or [],
483
+ files_touched=files_touched or [],
484
+ next_steps=next_steps or [],
485
+ )
486
+
487
+ self.handoffs.append(handoff)
488
+ self._save()
489
+ return handoff
490
+
491
+ def get_latest_handoff(self, to_agent: str) -> Optional[HandoffSummary]:
492
+ """Get the most recent handoff for an agent."""
493
+ for handoff in reversed(self.handoffs):
494
+ if handoff.to_agent == to_agent:
495
+ return handoff
496
+ return None
497
+
498
+ def get_handoffs_for_story(self, story_key: str) -> list[HandoffSummary]:
499
+ """Get all handoffs for a story."""
500
+ return [h for h in self.handoffs if h.story_key == story_key]
501
+
502
+ def to_context_string(self) -> str:
503
+ """Convert knowledge graph to context string for agents."""
504
+ lines = ["## Project Knowledge Base", ""]
505
+
506
+ # Active decisions
507
+ active = [d for d in self.decisions.values() if d.status == "active"]
508
+ if active:
509
+ lines.append("### Active Decisions")
510
+ for dec in sorted(active, key=lambda x: x.timestamp, reverse=True)[:10]:
511
+ lines.append(f"- **{dec.topic}** ({dec.agent}): {dec.decision}")
512
+ lines.append("")
513
+
514
+ # Recent handoffs
515
+ if self.handoffs:
516
+ lines.append("### Recent Handoffs")
517
+ for handoff in self.handoffs[-5:]:
518
+ lines.append(
519
+ f"- {handoff.from_agent} → {handoff.to_agent}: {handoff.summary[:100]}..."
520
+ )
521
+ lines.append("")
522
+
523
+ return "\n".join(lines)
524
+
525
+
526
+ # Convenience functions for quick access
527
+ def get_shared_memory(story_key: Optional[str] = None) -> SharedMemory:
528
+ """Get or create shared memory instance."""
529
+ return SharedMemory(story_key)
530
+
531
+
532
+ def get_knowledge_graph(story_key: Optional[str] = None) -> KnowledgeGraph:
533
+ """Get or create knowledge graph instance."""
534
+ return KnowledgeGraph(story_key)
535
+
536
+
537
+ def record_decision(
538
+ agent: str,
539
+ topic: str,
540
+ decision: str,
541
+ story_key: Optional[str] = None,
542
+ context: Optional[dict[str, Any]] = None,
543
+ ) -> Decision:
544
+ """Quick function to record a decision."""
545
+ kg = get_knowledge_graph(story_key)
546
+ return kg.add_decision(agent, topic, decision, context)
547
+
548
+
549
+ def share_learning(
550
+ agent: str, content: str, story_key: Optional[str] = None, tags: Optional[list[str]] = None
551
+ ) -> MemoryEntry:
552
+ """Quick function to share a learning."""
553
+ memory = get_shared_memory(story_key)
554
+ return memory.add(agent, content, tags)
555
+
556
+
557
+ def create_handoff(
558
+ from_agent: str, to_agent: str, story_key: str, summary: str, **kwargs
559
+ ) -> HandoffSummary:
560
+ """Quick function to create a handoff."""
561
+ kg = get_knowledge_graph(story_key)
562
+ return kg.add_handoff(from_agent, to_agent, story_key, summary, **kwargs)
563
+
564
+
565
+ def query_knowledge(question: str, story_key: Optional[str] = None) -> Optional[dict[str, Any]]:
566
+ """Quick function to query the knowledge graph."""
567
+ kg = get_knowledge_graph(story_key)
568
+ return kg.query(question)
569
+
570
+
571
+ if __name__ == "__main__":
572
+ # Demo usage
573
+ print("=== Shared Memory Demo ===\n")
574
+
575
+ # Create shared memory
576
+ memory = SharedMemory(story_key="demo-story")
577
+
578
+ # Add some entries
579
+ memory.add(
580
+ "ARCHITECT", "Decided to use PostgreSQL for user data", tags=["database", "decision"]
581
+ )
582
+ memory.add(
583
+ "DEV",
584
+ "Implemented user service with repository pattern",
585
+ tags=["implementation", "patterns"],
586
+ )
587
+ memory.add(
588
+ "REVIEWER", "Found missing input validation in auth module", tags=["review", "security"]
589
+ )
590
+
591
+ # Search
592
+ print("Search results for 'database':")
593
+ for entry in memory.search("database"):
594
+ print(f" - {entry.agent}: {entry.content}")
595
+
596
+ print("\n" + memory.to_context_string())
597
+
598
+ print("\n=== Knowledge Graph Demo ===\n")
599
+
600
+ # Create knowledge graph
601
+ kg = KnowledgeGraph(story_key="demo-story")
602
+
603
+ # Add decisions
604
+ kg.add_decision(
605
+ "ARCHITECT",
606
+ "authentication",
607
+ "Use JWT with refresh tokens",
608
+ context={"reason": "Stateless, scalable"},
609
+ )
610
+ kg.add_decision(
611
+ "ARCHITECT",
612
+ "database",
613
+ "PostgreSQL for user data",
614
+ context={"reason": "ACID compliance needed"},
615
+ )
616
+ kg.add_decision(
617
+ "DEV", "state-management", "Use Redux Toolkit", context={"reason": "Team familiarity"}
618
+ )
619
+
620
+ # Query
621
+ print("Query: 'What authentication approach was decided?'")
622
+ result = kg.query("What authentication approach was decided?")
623
+ if result:
624
+ print(f" Answer: {result['decision']} (by {result['agent']})")
625
+
626
+ # Create handoff
627
+ handoff = kg.add_handoff(
628
+ from_agent="SM",
629
+ to_agent="DEV",
630
+ story_key="demo-story",
631
+ summary="Story context created with all acceptance criteria defined",
632
+ key_decisions=["Use existing UserService", "Follow repository pattern"],
633
+ watch_out_for=["Rate limiting on profile uploads"],
634
+ next_steps=["Implement user profile endpoint", "Add validation", "Write tests"],
635
+ )
636
+
637
+ print("\n" + handoff.to_markdown())