diff --git a/muse/journal/SKILL.md b/muse/journal/SKILL.md
index 940611928..6f15f887c 100644
--- a/muse/journal/SKILL.md
+++ b/muse/journal/SKILL.md
@@ -90,7 +90,7 @@ sol call journal facet show # uses SOL_FACET
## facet create
```bash
-sol call journal facet create
[--emoji EMOJI] [--color COLOR] [--description DESC]
+sol call journal facet create [--emoji EMOJI] [--color COLOR] [--description DESC] [--consent]
```
Create a new facet directory and initial `facet.json`.
@@ -99,6 +99,7 @@ Create a new facet directory and initial `facet.json`.
- `--emoji`: optional icon emoji (default: `📦`).
- `--color`: optional hex color (default: `#667eea`).
- `--description`: optional description text.
+- `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry.
Examples:
@@ -130,11 +131,15 @@ sol call journal facet update work --description "Client work and planning" --em
## facet rename
```bash
-sol call journal facet rename
+sol call journal facet rename [--consent]
```
Rename a facet (directory and references in config/chat metadata).
+- `name`: current facet identifier.
+- `new-name`: new facet identifier.
+- `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry.
+
Example:
```bash
@@ -172,12 +177,13 @@ sol call journal facet unmute personal
## facet delete
```bash
-sol call journal facet delete [--yes]
+sol call journal facet delete [--yes] [--consent]
```
Delete a facet directory and all its data.
- `--yes`: skip confirmation prompt.
+- `--consent`: optional flag asserting explicit user approval was obtained; adds `"consent": true` to the audit log entry.
Example:
diff --git a/muse/unified.md b/muse/unified.md
index 65eeee7e4..6300138ef 100644
--- a/muse/unified.md
+++ b/muse/unified.md
@@ -83,7 +83,12 @@ For detailed responses, structure your answer for clarity — lead with the key
### Journal
- `sol call journal events [DAY] [-f FACET]` — List events with participants, times, and summaries.
- `sol call journal facet show [name]` — Show facet details.
-- `sol call journal facet create [--emoji EMOJI] [--color COLOR] [--description DESC]` — Create a new facet.
+- `sol call journal facet create [--emoji EMOJI] [--color COLOR] [--description DESC] [--consent]` — Create a new facet. Pass `--consent` to record explicit user approval in the audit log.
+- `sol call journal facet update [--title T] [--description D] [--emoji E] [--color C]` — Update facet metadata fields.
+- `sol call journal facet rename [--consent]` — Rename a facet. Pass `--consent` to record explicit user approval in the audit log.
+- `sol call journal facet mute ` — Hide a facet from default listings.
+- `sol call journal facet unmute ` — Show a previously muted facet in default listings.
+- `sol call journal facet delete [--yes] [--consent]` — Delete a facet and all its data. Pass `--consent` to record explicit user approval in the audit log.
- `sol call journal facets [--all]` — List facets.
### Awareness
diff --git a/tests/test_call.py b/tests/test_call.py
index 6d38624b5..68dbbe2f7 100644
--- a/tests/test_call.py
+++ b/tests/test_call.py
@@ -397,6 +397,110 @@ class TestFacetCRUD:
)
assert result.exit_code == 1
+ def test_facet_create_with_consent(self, facet_journal):
+ """Create with --consent records consent=True in log entry."""
+ from datetime import datetime
+
+ result = runner.invoke(
+ call_app, ["journal", "facet", "create", "Consent Facet", "--consent"]
+ )
+ assert result.exit_code == 0
+ today = datetime.now().strftime("%Y%m%d")
+ log_path = (
+ facet_journal / "facets" / "consent-facet" / "logs" / f"{today}.jsonl"
+ )
+ assert log_path.exists()
+ import json as _json
+
+ entries = [
+ _json.loads(line)
+ for line in log_path.read_text(encoding="utf-8").splitlines()
+ if line.strip()
+ ]
+ assert len(entries) == 1
+ assert entries[0]["action"] == "facet_create"
+ assert entries[0]["params"].get("consent") is True
+
+ def test_facet_create_without_consent(self, facet_journal):
+ """Create without --consent omits consent key from log entry."""
+ from datetime import datetime
+
+ result = runner.invoke(
+ call_app, ["journal", "facet", "create", "No Consent Facet"]
+ )
+ assert result.exit_code == 0
+ today = datetime.now().strftime("%Y%m%d")
+ log_path = (
+ facet_journal / "facets" / "no-consent-facet" / "logs" / f"{today}.jsonl"
+ )
+ assert log_path.exists()
+ import json as _json
+
+ entries = [
+ _json.loads(line)
+ for line in log_path.read_text(encoding="utf-8").splitlines()
+ if line.strip()
+ ]
+ assert len(entries) == 1
+ assert entries[0]["action"] == "facet_create"
+ assert "consent" not in entries[0]["params"]
+
+ def test_facet_rename_with_consent(self, facet_journal):
+ """Rename with --consent records consent=True in log entry."""
+ from datetime import datetime
+
+ result = runner.invoke(
+ call_app,
+ [
+ "journal",
+ "facet",
+ "rename",
+ "test-facet",
+ "renamed-consent",
+ "--consent",
+ ],
+ )
+ assert result.exit_code == 0
+ today = datetime.now().strftime("%Y%m%d")
+ log_path = (
+ facet_journal / "facets" / "renamed-consent" / "logs" / f"{today}.jsonl"
+ )
+ assert log_path.exists()
+ import json as _json
+
+ entries = [
+ _json.loads(line)
+ for line in log_path.read_text(encoding="utf-8").splitlines()
+ if line.strip()
+ ]
+ assert len(entries) == 1
+ assert entries[0]["action"] == "facet_rename"
+ assert entries[0]["params"].get("consent") is True
+
+ def test_facet_delete_with_consent(self, facet_journal):
+ """Delete with --consent records consent=True in journal-level log."""
+ from datetime import datetime
+
+ result = runner.invoke(
+ call_app,
+ ["journal", "facet", "delete", "test-facet", "--yes", "--consent"],
+ )
+ assert result.exit_code == 0
+ today = datetime.now().strftime("%Y%m%d")
+ log_path = facet_journal / "config" / "actions" / f"{today}.jsonl"
+ assert log_path.exists()
+ import json as _json
+
+ entries = [
+ _json.loads(line)
+ for line in log_path.read_text(encoding="utf-8").splitlines()
+ if line.strip()
+ ]
+ assert any(
+ e["action"] == "facet_delete" and e["params"].get("consent") is True
+ for e in entries
+ )
+
def test_facets_list_shows_metadata(self, facet_journal):
"""facets lists unmuted facets with metadata."""
result = runner.invoke(call_app, ["journal", "facets"])
diff --git a/think/facets.py b/think/facets.py
index 956953096..0985c5c91 100644
--- a/think/facets.py
+++ b/think/facets.py
@@ -677,6 +677,8 @@ def create_facet(
emoji: str = "📦",
color: str = "#667eea",
description: str = "",
+ *,
+ consent: bool = False,
) -> str:
"""Create a new facet directory with facet.json.
@@ -734,15 +736,18 @@ def create_facet(
pass
raise
+ log_params: dict = {
+ "title": title,
+ "emoji": emoji,
+ "color": color,
+ "description": description,
+ }
+ if consent:
+ log_params["consent"] = True
log_call_action(
facet=slug,
action="facet_create",
- params={
- "title": title,
- "emoji": emoji,
- "color": color,
- "description": description,
- },
+ params=log_params,
)
return slug
@@ -811,7 +816,7 @@ def update_facet(name: str, **kwargs: Any) -> dict[str, Any]:
return changed_fields
-def delete_facet(name: str) -> None:
+def delete_facet(name: str, *, consent: bool = False) -> None:
"""Delete a facet directory and clean up references.
Removes the facet directory tree and updates convey.json and chat metadata.
@@ -852,10 +857,13 @@ def delete_facet(name: str) -> None:
except (json.JSONDecodeError, OSError):
pass
+ log_params: dict = {"name": name}
+ if consent:
+ log_params["consent"] = True
log_call_action(
facet=None,
action="facet_delete",
- params={"name": name},
+ params=log_params,
)
shutil.rmtree(facet_path)
diff --git a/think/tools/call.py b/think/tools/call.py
index acbd4eed2..4954790f9 100644
--- a/think/tools/call.py
+++ b/think/tools/call.py
@@ -200,10 +200,21 @@ def create(
emoji: str = typer.Option("📦", "--emoji", help="Icon emoji."),
color: str = typer.Option("#667eea", "--color", help="Hex color."),
description: str = typer.Option("", "--description", help="Facet description."),
+ consent: bool = typer.Option(
+ False,
+ "--consent",
+ help="Assert that explicit user approval was obtained before calling this command (agent audit trail).",
+ ),
) -> None:
"""Create a new facet."""
try:
- slug = create_facet(title, emoji=emoji, color=color, description=description)
+ slug = create_facet(
+ title,
+ emoji=emoji,
+ color=color,
+ description=description,
+ consent=consent,
+ )
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1)
@@ -258,6 +269,11 @@ def update(
def rename(
name: str = typer.Argument(help="Current facet name."),
new_name: str = typer.Argument(help="New facet name."),
+ consent: bool = typer.Option(
+ False,
+ "--consent",
+ help="Assert that explicit user approval was obtained before calling this command (agent audit trail).",
+ ),
) -> None:
"""Rename a facet."""
try:
@@ -265,11 +281,10 @@ def rename(
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1)
- log_call_action(
- facet=new_name,
- action="facet_rename",
- params={"old_name": name, "new_name": new_name},
- )
+ params: dict = {"old_name": name, "new_name": new_name}
+ if consent:
+ params["consent"] = True
+ log_call_action(facet=new_name, action="facet_rename", params=params)
@facet_app.command()
@@ -298,6 +313,11 @@ def unmute(name: str = typer.Argument(help="Facet name to unmute.")) -> None:
def delete(
name: str = typer.Argument(help="Facet name to delete."),
yes: bool = typer.Option(False, "--yes", help="Skip confirmation."),
+ consent: bool = typer.Option(
+ False,
+ "--consent",
+ help="Assert that explicit user approval was obtained before calling this command (agent audit trail).",
+ ),
) -> None:
"""Delete a facet and all its data."""
if not yes:
@@ -309,7 +329,7 @@ def delete(
raise typer.Exit(1)
try:
- delete_facet(name)
+ delete_facet(name, consent=consent)
except FileNotFoundError:
typer.echo(f"Error: Facet '{name}' not found.", err=True)
raise typer.Exit(1)