|
9 | 9 | from datetime import datetime, timezone |
10 | 10 | from pathlib import Path |
11 | 11 | from typing import Any |
| 12 | +from uuid import uuid4 |
12 | 13 |
|
13 | 14 | from . import dogfood_cmd |
14 | 15 |
|
@@ -147,6 +148,95 @@ def _write_session_markdown(path: Path, *, title: str, payload: dict[str, Any], |
147 | 148 | path.write_text("\n".join(lines) + "\n") |
148 | 149 |
|
149 | 150 |
|
| 151 | +def _handoff_inbox(target: Path, payload: dict[str, Any], override: Path | None) -> Path: |
| 152 | + if override is not None: |
| 153 | + return override.expanduser() |
| 154 | + dogfood = payload.get("end", {}).get("dogfood", {}) |
| 155 | + configured = dogfood.get("handoff_inbox") |
| 156 | + if isinstance(configured, str) and configured: |
| 157 | + return Path(configured).expanduser() |
| 158 | + return target / ".claude" / "memory-handoffs" |
| 159 | + |
| 160 | + |
| 161 | +def _write_work_handoff(target: Path, session_dir: Path, payload: dict[str, Any], inbox: Path) -> Path: |
| 162 | + ended = payload.get("ended_at") or _now().isoformat() |
| 163 | + ended_slug = re.sub(r"[^0-9]", "", str(ended))[:12] or _now().strftime("%Y%m%d%H%M") |
| 164 | + title = payload.get("title") or payload.get("id") or "work-session" |
| 165 | + path = inbox / f"{ended_slug}-brigade-work-{_slug(str(title))}-{uuid4().hex[:6]}.md" |
| 166 | + end_snapshot = payload.get("end", {}) |
| 167 | + git = end_snapshot.get("git", {}) |
| 168 | + dogfood = end_snapshot.get("dogfood", {}) |
| 169 | + dirty = git.get("dirty_files") if isinstance(git, dict) else [] |
| 170 | + dirty_lines = "\n".join(f" - `{item}`" for item in dirty[:20]) if isinstance(dirty, list) else " - unavailable" |
| 171 | + latest = dogfood.get("latest_run") if isinstance(dogfood, dict) else None |
| 172 | + latest_line = "- latest run: none" |
| 173 | + if isinstance(latest, dict): |
| 174 | + latest_line = f"- latest run: `{latest.get('path')}` ({latest.get('status')})" |
| 175 | + next_step = dogfood.get("next") if isinstance(dogfood, dict) else None |
| 176 | + next_line = f"- next: {next_step}" if next_step else "- next: none extracted" |
| 177 | + note = payload.get("note") or "" |
| 178 | + document_content = f"""### Brigade work session: {payload.get('id')} |
| 179 | +- target: `{target}` |
| 180 | +- session artifacts: `{session_dir}` |
| 181 | +- branch: {git.get('branch') if isinstance(git, dict) else 'unknown'} |
| 182 | +- dirty files: {len(dirty) if isinstance(dirty, list) else 'unknown'} |
| 183 | +{latest_line} |
| 184 | +{next_line} |
| 185 | +""" |
| 186 | + if note: |
| 187 | + document_content += f"- note: {note}\n" |
| 188 | + body = f"""# Memory Handoff |
| 189 | +
|
| 190 | +## Type |
| 191 | +
|
| 192 | +workflow |
| 193 | +
|
| 194 | +## Title |
| 195 | +
|
| 196 | +Brigade work session ended: {_slug(str(title))} |
| 197 | +
|
| 198 | +## Summary |
| 199 | +
|
| 200 | +A Brigade work session was ended and local session artifacts were written. This handoff captures the session path, final git state, latest dogfood run, and extracted next step so the memory owner can route durable workflow context. |
| 201 | +
|
| 202 | +## Durable facts |
| 203 | +
|
| 204 | +- session: `{payload.get('id')}` |
| 205 | +- target: `{target}` |
| 206 | +- session artifacts: `{session_dir}` |
| 207 | +- status: {payload.get('status')} |
| 208 | +- started: {payload.get('started_at')} |
| 209 | +- ended: {payload.get('ended_at')} |
| 210 | +- note: {note or 'none'} |
| 211 | +- branch: {git.get('branch') if isinstance(git, dict) else 'unknown'} |
| 212 | +- dirty files: |
| 213 | +{dirty_lines} |
| 214 | +{latest_line} |
| 215 | +{next_line} |
| 216 | +
|
| 217 | +## Evidence |
| 218 | +
|
| 219 | +- session.json: `{session_dir / 'session.json'}` |
| 220 | +- start summary: `{session_dir / 'start.md'}` |
| 221 | +- end summary: `{session_dir / 'end.md'}` |
| 222 | +
|
| 223 | +## Recommended memory action |
| 224 | +
|
| 225 | +no-card |
| 226 | +
|
| 227 | +## Target document |
| 228 | +
|
| 229 | +.learnings/LEARNINGS.md |
| 230 | +
|
| 231 | +## Suggested document content |
| 232 | +
|
| 233 | +{document_content.strip()} |
| 234 | +""" |
| 235 | + inbox.mkdir(parents=True, exist_ok=True) |
| 236 | + path.write_text(body) |
| 237 | + return path |
| 238 | + |
| 239 | + |
150 | 240 | def _print_dirty(lines: list[str], *, limit: int) -> None: |
151 | 241 | print(f"dirty_files: {len(lines)}") |
152 | 242 | for line in lines[:limit]: |
@@ -188,7 +278,7 @@ def start(*, target: Path, title: str | None = None, force: bool = False) -> int |
188 | 278 | return 0 |
189 | 279 |
|
190 | 280 |
|
191 | | -def end(*, target: Path, note: str | None = None) -> int: |
| 281 | +def end(*, target: Path, note: str | None = None, handoff: bool = False, handoff_inbox: Path | None = None) -> int: |
192 | 282 | target = target.expanduser().resolve() |
193 | 283 | if not target.is_dir(): |
194 | 284 | print(f"error: --target is not a directory: {target}", file=sys.stderr) |
@@ -216,8 +306,15 @@ def end(*, target: Path, note: str | None = None) -> int: |
216 | 306 | payload["end"] = _session_snapshot(target) |
217 | 307 | _write_json(session_json, payload) |
218 | 308 | _write_session_markdown(session_dir / "end.md", title="Brigade Work Session End", payload=payload, key="end") |
| 309 | + if handoff: |
| 310 | + inbox = _handoff_inbox(target, payload, handoff_inbox) |
| 311 | + handoff_path = _write_work_handoff(target, session_dir, payload, inbox) |
| 312 | + payload["handoff"] = str(handoff_path) |
| 313 | + _write_json(session_json, payload) |
219 | 314 | current.unlink() |
220 | 315 | print(f"session: {session_dir}") |
| 316 | + if handoff: |
| 317 | + print(f"handoff: {payload['handoff']}") |
221 | 318 | print("status: ended") |
222 | 319 | return 0 |
223 | 320 |
|
|
0 commit comments