spec-kitty / spec-kitty/spec-kitty

Bug: `charter status` reports STALE from a retired comparison, and every recovery it names either refuses or no-ops

Open
#4,042 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
1.6k
Forks
165
Avg merge
13h 53m
Merged PRs (30d)
336

Description

# Bug: `charter status` reports STALE from a retired comparison, and every recovery it names either refuses or no-ops

## Summary

A project whose `charter.md` has been hand-edited reports `Status: STALE (modified since last sync)`
permanently, and none of the three commands involved can clear it. `charter status` directs the
operator to `charter sync`; `charter sync` refuses and directs them to `charter generate` or
`charter synthesize`; `charter generate` reports "Charter generated and synced" but writes only
`charter.yaml`, leaving the condition unchanged. The remaining suggestion, `charter synthesize`, is a
substantially heavier operation than the situation calls for. The result is a project that cannot
return to a clean charter state, so the STALE signal stops carrying information — a project can sit
STALE for weeks without anything being wrong.

## Reproduction

### Prerequisites

- `spec-kitty-cli 3.2.6` (release tag `v3.2.6`, SHA `5a746e768`)
- A project with a charter, where `charter.md` has been edited by hand — which is the supported way
to change charter prose, since `charter generate` does not rewrite `charter.md`

### Steps

```bash
$ spec-kitty charter status
Status: STALE (modified since last sync)
Expected hash: sha256:468008ea…
Current hash: sha256:7e1f0bf3…
Run: spec-kitty charter sync

$ spec-kitty charter sync
Charter was not synced — charter.md is stale and this command did not repair it.
Run 'spec-kitty charter generate' or 'spec-kitty charter synthesize' to repair it.

$ spec-kitty charter generate
Charter generated and synced
Files written:
✓ charter.yaml

$ spec-kitty charter status
Status: STALE (modified since last sync)
```

### Expected

Either a command clears the condition, or `status` does not recommend one that cannot.

### Actual

The loop closes on itself. `generate` additionally reports "generated and **synced**" while the sync
condition still holds, which reads as success.

## Root Cause

The STALE verdict is computed from a comparison the codebase describes as retired.

`charter_runtime/freshness/computer.py`, in `_compute_charter_source`:

> Landmine 2 (data-model.md): `charter.yaml` — not `charter.md` — is the authoritative, resolving
> charter source post-inversion. The historical `charter.md`-SHA-vs-`metadata.yaml::charter_hash`
> comparison is **retired outright** (module docstring); there is no self-referential hash to compute
> here. This sub-state can therefore only ever be `missing`, `invalid`, or `fresh` — **never
> `stale`**.

Yet `cli/commands/charter/status.py:84-86` still renders `Status: STALE (modified since last sync)`
from a `charter_sync` payload whose `status` is `"stale"`, and `charter status` prints an expected
and current hash pair — the very comparison described as retired.

`charter sync` cannot resolve it because it no longer syncs anything. Its own module docstring:

> Since the IC-04 triad retirement, `charter.sync.sync()` is a pure staleness reporter — `synced` is
> always `False` and `files_written` is empty.

So `sync` is a reporter whose name implies an action it can no longer take, and `status` recommends
it as the remedy. The same docstring records that this surface was already corrected once, under
#3045, for reporting success when nothing had been synced — the present behaviour is the same class
of mismatch one layer up: the human surface now fails loudly, but names a recovery that cannot work.

## Suggested Fix

The narrow fix is to stop reporting a verdict from a retired comparison: if
`charter.md`-vs-`charter_hash` is no longer authoritative, `charter status` should not surface it as
STALE, nor print the hash pair, and should not direct the operator to `charter sync`.

If the drift question still matters — and it plausibly does, since `charter.md` remains the file
humans edit — then the honest form is a distinct signal that says which artifact drifted from which,
resolved against `charter.yaml` as the authoritative source rather than against a retired hash.

Separately, `charter generate` should not print "generated and **synced**" when the sync condition it
names still holds afterwards. That single word is what makes the loop hard to diagnose: an operator
reasonably reads it as the repair having worked.

## Workaround Applied

None available. The charter's *content* can be corrected — editing `charter.md` and
`interview/answers.yaml` together, then running `charter generate` to refresh `charter.yaml`, leaves
every reader and agent seeing the right text. Only the STALE flag persists. Re-recording
`metadata.yaml::charter_hash` by hand would clear the display, but that is writing a value the tool
owns, and the comparison it feeds is retired in any case.

## Environment

- `spec-kitty-cli 3.2.6` (release tag `v3.2.6`, SHA `5a746e768`)
- Re-verified end-to-end 2026-09-08 on spec-kitty-cli 3.2.6 (Linux, Python 3.14): `charter status` →
STALE; `charter sync` → refuses and points to `generate`/`synthesize`; `charter generate` → prints
"Charter generated and synced" writing only `charter.yaml`; `charter status` → still STALE.
Originally observed on macOS (Darwin 25.6.0, Python 3.12).

---

**Reported by**: Kent Gale. Drafted with Claude Code (2026-09-05); re-verified on spec-kitty-cli 3.2.6 and filed with Claude Code (2026-09-08).
**Submission approved by**: Kent Gale (copy, 2026-09-05).

Contributor guide

Open the contributing guide

Research direction

Start with charter_runtime/freshness/computer.py and cli/commands/charter/status.py, then trace the charter sync and generate command entry points. Reproduce the status, sync, and generate sequence from the issue, and compare the displayed verdict, hashes, recovery guidance, and success message with the authoritative charter.yaml model. Done means the retired comparison is no longer presented as a repairable STALE condition and the command messaging is consistent.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.