FREEZE — musehub#215 muse branch --json created_by author fallback (Thinking)
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):
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 | Noneinbranch.py. - Use
sanitize_provenanceon the chosen handle (identity field; strips tab/newline/C0/C1). - Add
created_byto_LIST_REQUIRED_KEYSintests/test_branch_json_schema.py(currently omitscreated_by/intent/resumableeven though the TypedDict already has them). - New tests in
tests/test_cmd_branch.pyand/ortests/test_branch_json_schema.py.
Forbidden:
- Writing
created_by/created_atintoBranchMeta/.muse/config.toml. - Changing
muse push, fetch, clone, or MuseHub wire types. - Changing
committed_atin the JSON (it remains the tip timestamp). - Changing shelf's
created_byobject shape or calling_resolve_created_byfrom shelf. - Touching
task/video-timeline-vid1or any file outsidebranch.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
CommitRecordalready on disk. - Use
sanitize_provenance, not raw interpolation, so ESC/CSI cannot ridecreated_byinto 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 CommitRecords: 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.#216not 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 |
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