@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.
- package/CHANGELOG.md +526 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/devflow-checkpoint.js +10 -0
- package/bin/devflow-collab.js +10 -0
- package/bin/devflow-cost.js +10 -0
- package/bin/devflow-create-persona.js +10 -0
- package/bin/devflow-init.js +10 -0
- package/bin/devflow-memory.js +10 -0
- package/bin/devflow-new-doc.js +10 -0
- package/bin/devflow-personalize.js +10 -0
- package/bin/devflow-setup-checkpoint.js +10 -0
- package/bin/devflow-story.js +10 -0
- package/bin/devflow-tech-debt.js +10 -0
- package/bin/devflow-validate-overrides.js +10 -0
- package/bin/devflow-validate.js +10 -0
- package/bin/devflow-version.js +10 -0
- package/lib/constants.js +30 -0
- package/lib/exec-python.js +78 -0
- package/lib/python-check.js +178 -0
- package/package.json +64 -0
- package/tooling/.automation/agents/architect.md +135 -0
- package/tooling/.automation/agents/ba.md +70 -0
- package/tooling/.automation/agents/dev.md +79 -0
- package/tooling/.automation/agents/maintainer.md +97 -0
- package/tooling/.automation/agents/pm.md +116 -0
- package/tooling/.automation/agents/reviewer.md +141 -0
- package/tooling/.automation/agents/sm.md +61 -0
- package/tooling/.automation/agents/writer.md +193 -0
- package/tooling/.automation/config.ps1.template +61 -0
- package/tooling/.automation/config.sh.template +48 -0
- package/tooling/.automation/memory/.gitkeep +6 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
- package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
- package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
- package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
- package/tooling/.automation/overrides/templates/README.md +113 -0
- package/tooling/.automation/overrides/templates/architect/README.md +27 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
- package/tooling/.automation/overrides/templates/ba/README.md +27 -0
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
- package/tooling/.automation/overrides/templates/dev/README.md +32 -0
- package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
- package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
- package/tooling/.automation/overrides/templates/pm/README.md +27 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
- package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
- package/tooling/.automation/overrides/templates/sm/README.md +11 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
- package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
- package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
- package/tooling/.automation/overrides/templates/writer/README.md +27 -0
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
- package/tooling/completions/DevflowCompletion.ps1 +213 -0
- package/tooling/completions/_run-story +116 -0
- package/tooling/completions/run-story-completion.bash +136 -0
- package/tooling/docs/DOC-STANDARD.md +717 -0
- package/tooling/docs/sprint-status.yaml.template +24 -0
- package/tooling/docs/templates/bug-report.md +234 -0
- package/tooling/docs/templates/migration-spec.md +274 -0
- package/tooling/docs/templates/refactor-spec.md +86 -0
- package/tooling/docs/templates/tech-debt.md +86 -0
- package/tooling/scripts/context_checkpoint.py +556 -0
- package/tooling/scripts/cost_dashboard.py +617 -0
- package/tooling/scripts/create-persona.py +690 -0
- package/tooling/scripts/create-persona.sh +435 -0
- package/tooling/scripts/init-project-workflow.ps1 +651 -0
- package/tooling/scripts/init-project-workflow.py +70 -0
- package/tooling/scripts/init-project-workflow.sh +746 -0
- package/tooling/scripts/lib/__init__.py +35 -0
- package/tooling/scripts/lib/agent_handoff.py +526 -0
- package/tooling/scripts/lib/agent_router.py +698 -0
- package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
- package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
- package/tooling/scripts/lib/claude-cli.ps1 +952 -0
- package/tooling/scripts/lib/claude-cli.sh +1293 -0
- package/tooling/scripts/lib/cost_config.py +222 -0
- package/tooling/scripts/lib/cost_display.py +443 -0
- package/tooling/scripts/lib/cost_tracker.py +710 -0
- package/tooling/scripts/lib/currency_converter.py +328 -0
- package/tooling/scripts/lib/errors.py +438 -0
- package/tooling/scripts/lib/override-loader.sh +286 -0
- package/tooling/scripts/lib/pair_programming.py +589 -0
- package/tooling/scripts/lib/shared_memory.py +637 -0
- package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
- package/tooling/scripts/memory_summarize.py +324 -0
- package/tooling/scripts/new-doc.ps1 +405 -0
- package/tooling/scripts/new-doc.py +93 -0
- package/tooling/scripts/new-doc.sh +534 -0
- package/tooling/scripts/personalize_agent.py +385 -0
- package/tooling/scripts/rollback-migration.sh +540 -0
- package/tooling/scripts/run-collab.ps1 +251 -0
- package/tooling/scripts/run-collab.py +605 -0
- package/tooling/scripts/run-collab.sh +110 -0
- package/tooling/scripts/run-story.ps1 +490 -0
- package/tooling/scripts/run-story.py +387 -0
- package/tooling/scripts/run-story.sh +467 -0
- package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
- package/tooling/scripts/setup-checkpoint-service.py +87 -0
- package/tooling/scripts/setup-checkpoint-service.sh +236 -0
- package/tooling/scripts/tech-debt-tracker.py +608 -0
- package/tooling/scripts/update_version.py +244 -0
- package/tooling/scripts/validate-overrides.py +511 -0
- package/tooling/scripts/validate-overrides.sh +432 -0
- 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())
|