personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add facet consent audit flags

Add --consent to the journal facet create, rename, and delete CLI commands and thread consent through create_facet() and delete_facet() so audit entries record explicit approval without rewriting log files. Update the unified journal docs and journal skill to document the new flag on create, rename, and delete, and add facet CLI tests covering consent logging for create, rename, and delete.

+162 -19
+9 -3
muse/journal/SKILL.md
··· 90 90 ## facet create 91 91 92 92 ```bash 93 - sol call journal facet create <title> [--emoji EMOJI] [--color COLOR] [--description DESC] 93 + sol call journal facet create <title> [--emoji EMOJI] [--color COLOR] [--description DESC] [--consent] 94 94 ``` 95 95 96 96 Create a new facet directory and initial `facet.json`. ··· 99 99 - `--emoji`: optional icon emoji (default: `📦`). 100 100 - `--color`: optional hex color (default: `#667eea`). 101 101 - `--description`: optional description text. 102 + - `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry. 102 103 103 104 Examples: 104 105 ··· 130 131 ## facet rename 131 132 132 133 ```bash 133 - sol call journal facet rename <name> <new-name> 134 + sol call journal facet rename <name> <new-name> [--consent] 134 135 ``` 135 136 136 137 Rename a facet (directory and references in config/chat metadata). 137 138 139 + - `name`: current facet identifier. 140 + - `new-name`: new facet identifier. 141 + - `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry. 142 + 138 143 Example: 139 144 140 145 ```bash ··· 172 177 ## facet delete 173 178 174 179 ```bash 175 - sol call journal facet delete <name> [--yes] 180 + sol call journal facet delete <name> [--yes] [--consent] 176 181 ``` 177 182 178 183 Delete a facet directory and all its data. 179 184 180 185 - `--yes`: skip confirmation prompt. 186 + - `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry. 181 187 182 188 Example: 183 189
+6 -1
muse/unified.md
··· 83 83 ### Journal 84 84 - `sol call journal events [DAY] [-f FACET]` — List events with participants, times, and summaries. 85 85 - `sol call journal facet show [name]` — Show facet details. 86 - - `sol call journal facet create <title> [--emoji EMOJI] [--color COLOR] [--description DESC]` — Create a new facet. 86 + - `sol call journal facet create <title> [--emoji EMOJI] [--color COLOR] [--description DESC] [--consent]` — Create a new facet. Pass `--consent` to record explicit user approval in the audit log. 87 + - `sol call journal facet update <name> [--title T] [--description D] [--emoji E] [--color C]` — Update facet metadata fields. 88 + - `sol call journal facet rename <name> <new-name> [--consent]` — Rename a facet. Pass `--consent` to record explicit user approval in the audit log. 89 + - `sol call journal facet mute <name>` — Hide a facet from default listings. 90 + - `sol call journal facet unmute <name>` — Show a previously muted facet in default listings. 91 + - `sol call journal facet delete <name> [--yes] [--consent]` — Delete a facet and all its data. Pass `--consent` to record explicit user approval in the audit log. 87 92 - `sol call journal facets [--all]` — List facets. 88 93 89 94 ### Awareness
+104
tests/test_call.py
··· 397 397 ) 398 398 assert result.exit_code == 1 399 399 400 + def test_facet_create_with_consent(self, facet_journal): 401 + """Create with --consent records consent=True in log entry.""" 402 + from datetime import datetime 403 + 404 + result = runner.invoke( 405 + call_app, ["journal", "facet", "create", "Consent Facet", "--consent"] 406 + ) 407 + assert result.exit_code == 0 408 + today = datetime.now().strftime("%Y%m%d") 409 + log_path = ( 410 + facet_journal / "facets" / "consent-facet" / "logs" / f"{today}.jsonl" 411 + ) 412 + assert log_path.exists() 413 + import json as _json 414 + 415 + entries = [ 416 + _json.loads(line) 417 + for line in log_path.read_text(encoding="utf-8").splitlines() 418 + if line.strip() 419 + ] 420 + assert len(entries) == 1 421 + assert entries[0]["action"] == "facet_create" 422 + assert entries[0]["params"].get("consent") is True 423 + 424 + def test_facet_create_without_consent(self, facet_journal): 425 + """Create without --consent omits consent key from log entry.""" 426 + from datetime import datetime 427 + 428 + result = runner.invoke( 429 + call_app, ["journal", "facet", "create", "No Consent Facet"] 430 + ) 431 + assert result.exit_code == 0 432 + today = datetime.now().strftime("%Y%m%d") 433 + log_path = ( 434 + facet_journal / "facets" / "no-consent-facet" / "logs" / f"{today}.jsonl" 435 + ) 436 + assert log_path.exists() 437 + import json as _json 438 + 439 + entries = [ 440 + _json.loads(line) 441 + for line in log_path.read_text(encoding="utf-8").splitlines() 442 + if line.strip() 443 + ] 444 + assert len(entries) == 1 445 + assert entries[0]["action"] == "facet_create" 446 + assert "consent" not in entries[0]["params"] 447 + 448 + def test_facet_rename_with_consent(self, facet_journal): 449 + """Rename with --consent records consent=True in log entry.""" 450 + from datetime import datetime 451 + 452 + result = runner.invoke( 453 + call_app, 454 + [ 455 + "journal", 456 + "facet", 457 + "rename", 458 + "test-facet", 459 + "renamed-consent", 460 + "--consent", 461 + ], 462 + ) 463 + assert result.exit_code == 0 464 + today = datetime.now().strftime("%Y%m%d") 465 + log_path = ( 466 + facet_journal / "facets" / "renamed-consent" / "logs" / f"{today}.jsonl" 467 + ) 468 + assert log_path.exists() 469 + import json as _json 470 + 471 + entries = [ 472 + _json.loads(line) 473 + for line in log_path.read_text(encoding="utf-8").splitlines() 474 + if line.strip() 475 + ] 476 + assert len(entries) == 1 477 + assert entries[0]["action"] == "facet_rename" 478 + assert entries[0]["params"].get("consent") is True 479 + 480 + def test_facet_delete_with_consent(self, facet_journal): 481 + """Delete with --consent records consent=True in journal-level log.""" 482 + from datetime import datetime 483 + 484 + result = runner.invoke( 485 + call_app, 486 + ["journal", "facet", "delete", "test-facet", "--yes", "--consent"], 487 + ) 488 + assert result.exit_code == 0 489 + today = datetime.now().strftime("%Y%m%d") 490 + log_path = facet_journal / "config" / "actions" / f"{today}.jsonl" 491 + assert log_path.exists() 492 + import json as _json 493 + 494 + entries = [ 495 + _json.loads(line) 496 + for line in log_path.read_text(encoding="utf-8").splitlines() 497 + if line.strip() 498 + ] 499 + assert any( 500 + e["action"] == "facet_delete" and e["params"].get("consent") is True 501 + for e in entries 502 + ) 503 + 400 504 def test_facets_list_shows_metadata(self, facet_journal): 401 505 """facets lists unmuted facets with metadata.""" 402 506 result = runner.invoke(call_app, ["journal", "facets"])
+16 -8
think/facets.py
··· 677 677 emoji: str = "📦", 678 678 color: str = "#667eea", 679 679 description: str = "", 680 + *, 681 + consent: bool = False, 680 682 ) -> str: 681 683 """Create a new facet directory with facet.json. 682 684 ··· 734 736 pass 735 737 raise 736 738 739 + log_params: dict = { 740 + "title": title, 741 + "emoji": emoji, 742 + "color": color, 743 + "description": description, 744 + } 745 + if consent: 746 + log_params["consent"] = True 737 747 log_call_action( 738 748 facet=slug, 739 749 action="facet_create", 740 - params={ 741 - "title": title, 742 - "emoji": emoji, 743 - "color": color, 744 - "description": description, 745 - }, 750 + params=log_params, 746 751 ) 747 752 return slug 748 753 ··· 811 816 return changed_fields 812 817 813 818 814 - def delete_facet(name: str) -> None: 819 + def delete_facet(name: str, *, consent: bool = False) -> None: 815 820 """Delete a facet directory and clean up references. 816 821 817 822 Removes the facet directory tree and updates convey.json and chat metadata. ··· 852 857 except (json.JSONDecodeError, OSError): 853 858 pass 854 859 860 + log_params: dict = {"name": name} 861 + if consent: 862 + log_params["consent"] = True 855 863 log_call_action( 856 864 facet=None, 857 865 action="facet_delete", 858 - params={"name": name}, 866 + params=log_params, 859 867 ) 860 868 shutil.rmtree(facet_path) 861 869
+27 -7
think/tools/call.py
··· 200 200 emoji: str = typer.Option("📦", "--emoji", help="Icon emoji."), 201 201 color: str = typer.Option("#667eea", "--color", help="Hex color."), 202 202 description: str = typer.Option("", "--description", help="Facet description."), 203 + consent: bool = typer.Option( 204 + False, 205 + "--consent", 206 + help="Assert that explicit user approval was obtained before calling this command (agent audit trail).", 207 + ), 203 208 ) -> None: 204 209 """Create a new facet.""" 205 210 try: 206 - slug = create_facet(title, emoji=emoji, color=color, description=description) 211 + slug = create_facet( 212 + title, 213 + emoji=emoji, 214 + color=color, 215 + description=description, 216 + consent=consent, 217 + ) 207 218 except ValueError as e: 208 219 typer.echo(f"Error: {e}", err=True) 209 220 raise typer.Exit(1) ··· 258 269 def rename( 259 270 name: str = typer.Argument(help="Current facet name."), 260 271 new_name: str = typer.Argument(help="New facet name."), 272 + consent: bool = typer.Option( 273 + False, 274 + "--consent", 275 + help="Assert that explicit user approval was obtained before calling this command (agent audit trail).", 276 + ), 261 277 ) -> None: 262 278 """Rename a facet.""" 263 279 try: ··· 265 281 except ValueError as e: 266 282 typer.echo(f"Error: {e}", err=True) 267 283 raise typer.Exit(1) 268 - log_call_action( 269 - facet=new_name, 270 - action="facet_rename", 271 - params={"old_name": name, "new_name": new_name}, 272 - ) 284 + params: dict = {"old_name": name, "new_name": new_name} 285 + if consent: 286 + params["consent"] = True 287 + log_call_action(facet=new_name, action="facet_rename", params=params) 273 288 274 289 275 290 @facet_app.command() ··· 298 313 def delete( 299 314 name: str = typer.Argument(help="Facet name to delete."), 300 315 yes: bool = typer.Option(False, "--yes", help="Skip confirmation."), 316 + consent: bool = typer.Option( 317 + False, 318 + "--consent", 319 + help="Assert that explicit user approval was obtained before calling this command (agent audit trail).", 320 + ), 301 321 ) -> None: 302 322 """Delete a facet and all its data.""" 303 323 if not yes: ··· 309 329 raise typer.Exit(1) 310 330 311 331 try: 312 - delete_facet(name) 332 + delete_facet(name, consent=consent) 313 333 except FileNotFoundError: 314 334 typer.echo(f"Error: Facet '{name}' not found.", err=True) 315 335 raise typer.Exit(1)