From b56841c5f4be5a574f539cd343008a29f7f613c0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 20:03:32 +0900 Subject: [PATCH] =?UTF-8?q?ROADMAP=20#125:=20git=5Fstate=20'clean'=20emitt?= =?UTF-8?q?ed=20for=20non-git=20directories;=20GitWorkspaceSummary=20defau?= =?UTF-8?q?lt=20all-zeros=20=E2=86=92=20is=5Fclean()=20=E2=86=92=20'clean'?= =?UTF-8?q?=20even=20when=20in=5Fgit=5Frepo:=20false;=20contradictory=20do?= =?UTF-8?q?ctor=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dogfooded 2026-04-18 on main HEAD debbcbe from /tmp/cdBB2. Non-git directory: $ mkdir /tmp/cdBB2 && cd /tmp/cdBB2 # NO git init $ claw --output-format json status | jq .workspace.git_state 'clean' # should be null — not in a git repo $ claw --output-format json doctor | jq '.checks[] | select(.name=="workspace") | {in_git_repo, git_state}' {"in_git_repo": false, "git_state": "clean"} # CONTRADICTORY: not in git BUT git is 'clean' Trace: main.rs:2550-2554 parse_git_workspace_summary: let Some(status) = status else { return summary; // all-zero default when no git }; All-zero GitWorkspaceSummary → is_clean() (changed_files==0) → true → headline() = 'clean' main.rs:4950 status JSON: git_summary.headline() for git_state main.rs:1856 doctor workspace: same headline() for git_state Fix shape (~25 lines): - Return Option when status is None - headline() returns Option: None when no git - Status JSON: git_state: null when not in git - Doctor: omit git_state when in_git_repo: false, or set null - Optional: claw init skip .gitignore in non-git dirs - Regression: non-git → null, git clean → 'clean', detached HEAD → 'clean' + 'detached HEAD' Joins Truth-audit — 'clean' is a lie for non-git dirs. Adjacent to #89 (claw blind to mid-rebase) — same field, different missing state. Joins #100 (status/doctor JSON gaps) — another field whose value doesn't reflect reality. Natural bundle: #89 + #100 + #125 — git-state-completeness triple: rebase/merge invisible (#89) + stale-base unplumbed (#100) + non-git 'clean' lie (#125). Complete git_state field failure coverage. Filed in response to Clawhip pinpoint nudge 1495016073085583442 in #clawcode-building-in-public. --- ROADMAP.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 8ab7835..6df8d4d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4482,3 +4482,71 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label], **Blocker.** None. Localized across parse + status JSON + doctor check. **Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdAA2` on main HEAD `bb76ec9` in response to Clawhip pinpoint nudge at `1495000973914144819`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122, #123) as 17th — `--model` silently accepts garbage with no validation. Joins **Truth-audit / diagnostic-integrity** — status JSON model field has no provenance. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122, #123) as 10th — `--model` flag, `.claw.json model`, and the default model are three sources that disagree (#105 adjacent). Natural bundle: **#105 + #124** — model-resolution pair: 4-surface disagreement (#105) + no validation + no provenance (#124). Also **#122 + #124** — unvalidated-flag pair: `--base-commit` accepts anything (#122) + `--model` accepts anything (#124). Same parser pattern. Session tally: ROADMAP #124. + +125. **`git_state: "clean"` is emitted by both `status` and `doctor` JSON even when `in_git_repo: false` — a non-git directory reports the same sentinel as a git repo with no changes. `GitWorkspaceSummary::default()` returns all-zero fields; `is_clean()` checks `changed_files == 0` → true → `headline() = "clean"`. A claw checking `if git_state == "clean" then proceed` would proceed even in a non-git directory. Doctor correctly surfaces `in_git_repo: false` and `summary: "current directory is not inside a git project"`, but the `git_state` field contradicts this by claiming "clean." Separately, `claw init` creates a `.gitignore` file even in non-git directories — not harmful (ready for future `git init`) but misleading** — dogfooded 2026-04-18 on main HEAD `debbcbe` from `/tmp/cdBB2`. + + **Concrete repro.** + ``` + $ mkdir /tmp/cdBB2 && cd /tmp/cdBB2 + # NO git init — bare directory + + $ claw init + Init + Project /private/tmp/cdBB2 + .claw/ created + .claw.json created + .gitignore created # created in non-git dir + CLAUDE.md created + + $ claw --output-format json status | jq '{git_branch: .workspace.git_branch, git_state: .workspace.git_state, project_root: .workspace.project_root}' + {"git_branch": null, "git_state": "clean", "project_root": null} + # git_state: "clean" despite NO GIT REPO. + + $ claw --output-format json doctor | jq '.checks[] | select(.name=="workspace") | {in_git_repo, git_state, status, summary}' + {"in_git_repo": false, "git_state": "clean", "status": "warn", "summary": "current directory is not inside a git project"} + # in_git_repo: false BUT git_state: "clean" + # status: "warn" + summary: "not inside a git project" — CONTRADICTS git_state "clean" + ``` + + **Trace path.** + - `rust/crates/rusty-claude-cli/src/main.rs:2550-2554` — `parse_git_workspace_summary`: + ```rust + fn parse_git_workspace_summary(status: Option<&str>) -> GitWorkspaceSummary { + let mut summary = GitWorkspaceSummary::default(); + let Some(status) = status else { + return summary; // returns all-zero default when no git + }; + ``` + When `project_context.git_status` is `None` (non-git), returns `GitWorkspaceSummary { changed_files: 0, staged_files: 0, unstaged_files: 0, ... }`. + - `rust/crates/rusty-claude-cli/src/main.rs:2348-2355` — `GitWorkspaceSummary::headline`: + ```rust + fn headline(self) -> String { + if self.is_clean() { + "clean".to_string() + } else { ... } + } + ``` + `is_clean()` = `changed_files == 0` → true for all-zero default → returns "clean" even when there's no git. + - `rust/crates/rusty-claude-cli/src/main.rs:4950` — status JSON builder uses `context.git_summary.headline()` for the `git_state` field. + - `rust/crates/rusty-claude-cli/src/main.rs:1856` — doctor workspace check uses the same `headline()` for the `git_state` field, alongside the separate `in_git_repo: false` field. + + **Why this is specifically a clawability gap.** + 1. *False positive "clean" on non-git directories.* A claw preflighting with `git_state == "clean" && project_root != null` would work. But a claw checking ONLY `git_state == "clean"` (the simpler, more obvious check) would proceed in non-git directories. The `null` project_root is the real guard, but git_state misleads. + 2. *Contradictory fields in doctor.* `in_git_repo: false` + `git_state: "clean"` in the same check. A claw reading one field gets "not in git"; reading the other gets "git is clean." The two fields should be consistent or `git_state` should be `null`/absent when `in_git_repo` is false. + 3. *Joins truth-audit.* The "clean" sentinel is a truth claim about git state. When there's no git, the claim is vacuously true at best, actively misleading at worst. + 4. *Adjacent to #89 (claw blind to mid-rebase/merge).* #89 said git_state doesn't capture rebase/merge/cherry-pick. **#125** says git_state also doesn't capture "not in git" — another missing state. + 5. *Minor: `claw init` creates `.gitignore` without git.* Not harmful but joins the pattern of init producing artifacts for absent subsystems (`.gitignore` without git, `.claw.json` with `dontAsk` per #115). + + **Fix shape — null `git_state` when not in git repo.** + 1. *Return `None` from `parse_git_workspace_summary` when status is `None`.* Change return type to `Option`. ~10 lines. + 2. *`headline()` returns `Option`.* `None` when no git, `Some("clean")` / `Some("dirty · ...")` when in git. ~5 lines. + 3. *Status JSON: `git_state: null` when not in git.* Currently always a string. ~3 lines. + 4. *Doctor check: omit `git_state` field entirely when `in_git_repo: false`.* Or set to `null` / `"no-git"`. ~3 lines. + 5. *Optional: `claw init` warns when creating `.gitignore` in non-git directory.* Or: skip `.gitignore` creation when not in git. ~5 lines. + 6. *Regression tests.* (a) Non-git directory → `git_state: null` (not "clean"). (b) Git repo with clean state → `git_state: "clean"`. (c) Detached HEAD → `git_state: "clean"` + `git_branch: "detached HEAD"` (current behavior, already correct). + + **Acceptance.** `claw --output-format json status` in a non-git directory shows `git_state: null` (not "clean"). Doctor workspace check with `in_git_repo: false` has `git_state: null` (or absent). A claw checking `git_state == "clean"` correctly rejects non-git directories. + + **Blocker.** None. ~25 lines across two files. + + **Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdBB2` on main HEAD `debbcbe` in response to Clawhip pinpoint nudge at `1495016073085583442`. Joins **Truth-audit / diagnostic-integrity** — `git_state: "clean"` is a lie for non-git directories. Adjacent to **#89** (claw blind to mid-rebase) — same field, different missing state. Joins **#100** (status/doctor JSON gaps) — another field whose value doesn't reflect reality. Natural bundle: **#89 + #100 + #125** — git-state-completeness triple: rebase/merge invisible (#89) + stale-base unplumbed (#100) + non-git "clean" lie (#125). Complete coverage of git_state field failures. Session tally: ROADMAP #125.