# FREEZE — musehub#215 `muse branch --json` created_by author fallback (Thinking) ```yaml id: muse-215-created-by-fallback model: Thinking frozen: true status: freeze-review-pass date: 2026-09-17 author: aaronrene repo: gabriel/muse ticket: https://staging.musehub.ai/gabriel/musehub/issues/215 consumes: - gabriel comment sha256:a4b55431b9694cec98d3d0b900214b68cdeb75e13c3be37dd7c14f27129e59dd escalation: none tier3_gates: - merge to gabriel/muse main (Gabriel review) blocked_by: freeze review must be pass before {step}b Auto out_of_scope: - musehub#216 BranchMeta persistence - push/clone protocol - changing created_by JSON type from str | None to an object ``` ## 1. WHAT is frozen **Statement of truth `{step}b` Auto consumes without re-deriving:** `muse branch --json`'s `created_by` field is **tip-commit authorship**, not branch-creation provenance. Today it reads only `CommitRecord.agent_id` (`muse/cli/commands/branch.py:872` on this tree; `:854` on installed 0.2.0rc15): ```python created_by: str | None = (rec.agent_id if rec and rec.agent_id else None) ``` `CommitRecord` (`muse/core/commits.py`) has two authorship fields: | Field | Default | Meaning | | --- | --- | --- | | `author: str` | `""` | human handle (e.g. `"aaronrene"`) | | `agent_id: str` | `""` | agent identity (e.g. `"claude-code"`) | When the tip is human-authored, `agent_id` is `""` and `created_by` is unconditionally `null` even though `.author` is populated. **Reproduced 2026-09-17** in a throwaway repo (installed `muse 0.2.0rc15`): | Tip | `author` | `agent_id` | `created_by` today | | --- | --- | --- | --- | | `muse commit -m "human authored tip"` | `aaronrene` | `""` | **`null`** | ### Frozen resolution rule For each listed branch, after `rec = read_commit(...)` (or `None` if no tip): ``` created_by = _first_nonempty(rec.agent_id, rec.author) if rec else None ``` where `_first_nonempty(*values)` returns the first value whose `str.strip()` is non-empty, then passed through `sanitize_provenance` (`muse/core/validation.py:402`), else `None`. Equivalence with Gabriel's suggested `rec.agent_id or rec.author or None` for the empty-string case. **Stricter than raw `or` on whitespace-only strings:** `" "` is absent, not a handle. Precedence: **agent_id wins** when both are non-empty after strip. That matches the ticket. ### JSON contract (unchanged type) `_BranchEntryJson.created_by` stays `str | None` (`branch.py:104`). Do **not** change it to the shelf object `{"handle", "kind"}`. That shape is `#216`. Human (non-`--json`) `muse branch` output is unchanged. This ticket is the JSON field only. ## 2. HOW is frozen **Single edit site:** the assignment at `muse/cli/commands/branch.py:872` (and any tiny helper extracted next to it in the same module if tests need a unit seam). **Allowed:** - Extract `_created_by_from_record(rec: CommitRecord | None) -> str | None` in `branch.py`. - Use `sanitize_provenance` on the chosen handle (identity field; strips tab/newline/C0/C1). - Add `created_by` to `_LIST_REQUIRED_KEYS` in `tests/test_branch_json_schema.py` (currently omits `created_by` / `intent` / `resumable` even though the TypedDict already has them). - New tests in `tests/test_cmd_branch.py` and/or `tests/test_branch_json_schema.py`. **Forbidden:** - Writing `created_by` / `created_at` into `BranchMeta` / `.muse/config.toml`. - Changing `muse push`, fetch, clone, or MuseHub wire types. - Changing `committed_at` in the JSON (it remains the **tip** timestamp). - Changing shelf's `created_by` object shape or calling `_resolve_created_by` from shelf. - Touching `task/video-timeline-vid1` or any file outside `branch.py` + the test files named above + this freeze + living docs. ## 3. Fail-closed rules | # | Condition | Result | | --- | --- | --- | | F1 | `rec is None` (empty / never-committed branch) | `created_by` is JSON `null` | | F2 | `agent_id` strip-empty, `author` strip-empty | `null` | | F3 | `agent_id` strip-empty, `author` non-empty | sanitized `author` | | F4 | `agent_id` non-empty (author anything) | sanitized `agent_id` | | F5 | Control characters in the chosen handle | stripped by `sanitize_provenance`; if nothing remains, `null` | | F6 | Exception reading the tip | keep existing `branch --json` error behavior; do not invent a handle | No new network calls. No new config keys. ## 4. Security - Display-only. Source is the local `CommitRecord` already on disk. - Use `sanitize_provenance`, not raw interpolation, so ESC/CSI cannot ride `created_by` into a consuming TTY or log (`validation.py:402–410`). - Do not log full commit records in test failure messages beyond the two authorship fields. - No secrets, no auth change, no wire change. Escalation: **none**. ## 5. Seven-tier test matrix | Tier | Proves | How | | --- | --- | --- | | 1 unit | F1–F5 on `_created_by_from_record` (or the assignment) with constructed `CommitRecord`s: None; both empty; author only; agent only; both set (agent wins); whitespace-only author; agent with ESC byte | `tests/test_cmd_branch.py` | | 2 integration | `muse branch --json` via `CliRunner` on a fixture repo after a human `commit` (no `--agent-id`) returns a non-null `created_by` equal to `user.handle` / commit author | extend `test_branch_json_schema.py` | | 3 e2e | Throwaway repo: human commit → `created_by == author`; second commit with `--agent-id` → `created_by == agent_id` on the same branch | CLI e2e in the same test module | | 4 stress | 200 branches listed via `--json` completes; every entry has the `created_by` key | cap runtime, no OOM | | 5 data-integrity | Run `--json` twice on an unchanged repo → byte-identical `created_by` values; after a new human commit on a previously agent tip, `created_by` **does** change (tip-derived; this ticket does not freeze creation-time) | assert both | | 6 performance | 200-branch list < 5s on the test fixture | wall clock bound | | 7 security | ESC in `author` does not appear in JSON; no `MUSE_*` / key material in output | `sanitize_provenance` assertion | Existing `TestListSchemaI` must keep passing. Adding `created_by` to `_LIST_REQUIRED_KEYS` is required so a future regression cannot drop the key. ## 6. Definition of Done (for `{step}b`, not this freeze) - All seven tiers green locally. - `/build-verification-review` → **pass**. - `#216` not started. - Living docs updated together. - Feature-branch commit on `feat/215-branch-json-created-by-fallback`. ## 7. Review discipline (file+line citations) `ok review --freeze docs/MUSE-215-CREATED-BY-FALLBACK-FREEZE.md` must return **pass** before any edit to `branch.py` beyond a helper that is specified here. | Claim | file+line | | --- | --- | | `created_by` reads only `agent_id` | `muse/cli/commands/branch.py:872` | | JSON type `str \| None` | `muse/cli/commands/branch.py:104` | | `CommitRecord.author` / `agent_id` | `muse/core/commits.py:210` / `:215` | | `sanitize_provenance` | `muse/core/validation.py:402` | | schema test omits `created_by` | `tests/test_branch_json_schema.py:39-41` | | `BranchMeta` has no creation fields | `muse/cli/config.py:121-139` | ```yaml review_stamp: gate: mechanical reviewed_at: '2026-09-17T10:43:21Z' mechanical_verdict: pass produced_by: checklist_engine provider_kind: rule_engine reviewer_mode: agent reviewer_model: null reviewer_provider: local checklist_ids: - C1 - C2 - C3 - C4 - C5 - C6 - C7 - C8 checklist_source: builtin findings_count: 0 override_applied: false kit_version: 0.1.0 artifact_digest: sha256:009d150f71f7b921cb6089ebf6b800bc3f3e7905b042d682998fc2af763f2093 ```