* docs(#3812): say that Current Position is single-valued, and pin the behavior that makes it true #3812 shipped CLOSED with half its acceptance unmet. #3873 delivered cardinality for FRONTMATTER keys - current_phase/current_plan render as optional at docs/reference/state-md.md:89,91, covered by tests/gen-state-md-docs.test.cjs:374. The issue's actual ask was the ## Current Position BODY section, and that never landed. Surfaced by an /adr-phase-coverage audit of epic #3473; the issue was reopened rather than noted. The section now states three things: every field is single-valued, the section is overwritten rather than appended to, and a duplicate resolves to the FIRST occurrence with no warning - so a line appended in good faith is silently ignored rather than winning. Progress history belongs in ## Performance Metrics, two headings down, and the text now points there. The third claim is a behavioral promise about the reader, so it was VERIFIED BY EXECUTION before being written rather than inferred from the issue title: stateExtractField(<"Phase: 1 of 5 (First)" ... "Phase: 9 of 9 (Appended later)">, "Phase") -> "1 of 5 (First)" The mechanism is state-document.cjs:405 - the plain-line pattern ^<field>:[ \t]*(.+) carries flags im with NO g, so String.match returns the first hit. Writing "first wins" without running it would have repeated the exact error I had to retract twice in this epic already. A test pins the reader, not the prose. Three rows in tests/state.test.cjs: T1 (load-bearing) asserts the duplicated case resolves first; T2 asserts the ordinary single-field case still works, so a fix that only functions when duplicated cannot pass; T3 puts a Plan: line BETWEEN the two Phase: lines and asserts it resolves independently - negative space, because a reader returning the first line of the SECTION rather than the first matching FIELD would satisfy T1 alone. Proven to discriminate: a last-match variant returns "9 of 9 (Appended later)" and T1 reds. No assertion checks that the document contains a sentence. That is what local/no-source-grep exists to stop, and it would pin wording that is allowed to improve. The point of the test is that if that regex ever gains g and a last-match walk, the test fails - instead of the documentation quietly becoming a lie with nothing to notice. Prose only, no new heading. docs-state-md-locale-parity compares heading-level sequences by LCS rather than text, so added paragraphs cannot fail it while an added HEADING would fail all four locales. The constraint is structural, not stylistic - confirmed by running that comparison after the edit. The four locale copies are translated rather than left stale. They are not gate-enforced for prose, so "nothing fails" was available and is not the same as correct: leaving four documents asserting something the English one now contradicts is a correctness problem. Code spans and the anchor link stay untranslated - they name real tokens. The whole approach rests on one fact, checked first: ## Current Position at :196-208 sits OUTSIDE every generated marker region (:81-104, :138-151), so a hand edit survives --write. Re-confirmed after all five edits - gen-state-md-docs --check reports all 6 targets up to date. Had that been false the fix would have belonged in the generator, and a hand edit would have been silently reverted. One real gate failure fixed inline rather than reported: the new test's comments referenced docs/reference/state-md.md, which was not in that file's registered exempt-docs paths, and lint-docs-guard-registration failed lint:ci correctly. Registered. Known limit, named rather than folded in: gsd-tools validate/health still do NOT warn on a duplicated Phase:. #3812 records that as a "consider", not a requirement, and confirms none of the nine rules in src/health-diagnostic-rules/{state-consistency,phase-structure}.cts counts occurrences. Documenting the silent first-match is the delivered scope; making it loud is new scope and stays unclaimed. Closes #3812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#3812): the rule I documented was false — replace it with the measured one An isolated review returned two blockers. Both mine, and the first is the worse kind: I wrote a falsifiable rule into a reference page and got it wrong. 1. "A duplicate resolves to the FIRST occurrence" is FALSE. stateExtractField (src/state-document.cts:401-419) tries BOLD `**F:**` across the whole input, THEN plain `^F:`, THEN a pipe-table row. Form precedence beats document order. Measured against the built reader, all intra-section: Phase: A (plain) / **Phase:** B (bold, later) -> B LATER WINS Phase: A (indented) / Phase: B (plain, later) -> B LATER WINS Phase: A (plain) / | Phase | T (table) | -> A first wins My original verification tested plain-versus-plain, saw first-wins, and generalized to all forms. Measuring one case and claiming the general rule is the same error I have had to retract twice already in this epic. It is also worse than silence. The sentence told authors an appended line is safely ignored; a bold line appended "for emphasis" silently overrides the original. Someone trusting the doc would have corrupted their own state file. And #3812 never asked for a resolution rule - it asked for single-valued, overwrite-not-append, and where history goes. The rule was my unrequested addition. Replaced with the measured truth: resolution is by FORM (bold anywhere, then plain at line-start, then table row), and only WITHIN the winning form does the first occurrence win. Both consequences stated plainly - a higher-ranked form wins regardless of position, and an indented `Phase:` is invisible to the plain form. All five claims in the new paragraph verified by execution before being written, including the two I had wrong. 2. The tests tested the wrong case and passed for the wrong reason. T1/T3 put the second `Phase:` under `## Somewhere else` - the INTER-section case, which #2956 already fixed by scoping. #3812 says verbatim that #2956 "fixed the inter-section case and never addressed intra-section duplication", so the case the new prose describes was untested, and the fixtures passed because of section scoping rather than field resolution. They also called bare stateExtractField rather than the production chain, T2 could not discriminate first from last at all, and no fixture mixed forms - which is precisely why the false claim survived to review. Rewritten as four rows, all intra-section, all through the real stateCurrentPositionSlice -> stateExtractField path: plain-then-plain (first wins within a form), plain-then-bold (the bold LATER value wins - the row whose absence let the false claim ship), indented-then-plain (indented invisible), and sibling-field independence. Each proven to fail against a reader that disagrees. 3. Two dead anchors. pt-BR and zh-CN linked `#performance-metrics` while their own headings are `### Métricas de Desempenho` and `### 性能指标`. Both fixed to the anchor their own heading generates. ja-JP/ko-KR kept the English heading, so theirs already resolved. 4. A ja/ko sentence inverted its own meaning. Both rendered "which is the section designed to grow" with a bare demonstrative whose nearest referent read as Current Position - saying the opposite of the point. Rewritten so the clause attaches unambiguously to `## Performance Metrics`. 5. Cross-locale drift, flagged by the implementing agent rather than by me: after fixing EN, the four locales still stated the OLD false rule. Four documents asserting something measured to be wrong is worse than four saying nothing. All four now carry a faithful translation of the corrected paragraph, with code spans, each file's own anchor, and the ja/ko referent fix preserved. Verified: all five claims executed against the built reader; every rewritten test row proven to discriminate; gen-state-md-docs --check reports all 6 targets up to date, so the edits stay outside the generated marker regions; locale heading parity unaffected (prose only, no headings added); build:lib, lint and lint:ci all exit 0. Refs #3812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#3812): second false rule on the same page — scope the ranking to the section A second isolated review found a second false falsifiable claim, and the failure mode is the same one twice in a row: attempt 1: verified plain-vs-plain, wrote a claim about ALL FORMS attempt 2: verified bare stateExtractField, wrote a claim about THE DOCUMENT Both times the claim covered a wider surface than what was actually executed. The fix each time was not a better sentence, it was executing the surface the sentence describes. BLOCKER — "bold `**Phase:**` anywhere in the DOCUMENT wins" is false. ## Current Position / Phase: 1 of 5 + ## Archive / **Phase:** 88 bare stateExtractField(whole doc) -> "88 (other section)" PRODUCTION (slice then extract) -> "1 of 5 (in section)" #2956's section slice means production never hands another section to the matcher; a bold line in `## Archive`, or in the YAML frontmatter, is simply not seen. The ranking is real but scoped: it applies WITHIN `## Current Position`. I verified against the bare function and wrote a claim about the system. Every existing test placed its bold line inside the section, which is exactly why nothing contradicted the claim. T5 now puts a bold `**Phase:**` in `## Archive` and asserts production returns the in-section plain value, with the unscoped reader asserted to DISAGREE so the row proves the scoping rather than assuming it. BLOCKER — the changeset still shipped the ORIGINAL retracted claim. I corrected the page and left the release note saying "resolves to the first occurrence ... a second entry added in good faith is silently ignored". The note contradicted the page it announces, and the release note is what most people actually read. Rewritten to the corrected rule. MEDIUM — the concession was inverted. It read "wins even if it comes FIRST in the file", which is the vacuous direction; the surprising case, and the one the very next clause illustrates with an APPENDED bold line, is "even if it comes LAST". All four locales reproduced the inversion faithfully, so it was an EN-source defect rather than translation drift. Two sharp edges now named, both measured: a bold `**Phase:**` followed only by trailing spaces resolves to an EMPTY STRING and does not fall through to a valid plain line below (T6 pins it); and `| **Phase:** | 3 of 4 |` short-circuits to the bold form and returns the literal `"| 3 of 4 |"`. A page that teaches form ranking has to say where the ranking bites. Also fixed: all five files labelled the link `## Performance Metrics` while the heading is `### Performance Metrics`. Anchors resolved correctly everywhere; only the label's level was wrong. Every clause in the final paragraph re-verified through the PRODUCTION chain (stateCurrentPositionSlice -> stateExtractField), clause by clause, before being written: bold in another section does not win; bold in frontmatter does not win; bold appended last does win; first wins within one form; trailing-space bold yields empty. All four locales carry the same corrected rule. gen-state-md-docs --check reports all 6 targets up to date; heading counts unchanged at 20/20 across all five files, so locale heading-parity is untouched; build:lib, lint and lint:ci all exit 0. Refs #3812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3812): backfill changeset pr number Refs #3812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
17 KiB
Referência do esquema STATE.md
STATE.md é o arquivo de memória viva do projeto do GSD Core — um único documento Markdown que registra em que ponto o projeto se encontra, o que aconteceu por último e o que executar a seguir. Esta página documenta sua estrutura. Consulte o índice da documentação.
Visão geral
Todo projeto gerenciado pelo GSD Core mantém um único STATE.md em .planning/STATE.md. Ele é lido no início de todo fluxo de trabalho e escrito após toda ação significativa. O arquivo combina:
- Frontmatter YAML — campos legíveis por máquina consumidos pelo hook de linha de status (
parseStateMd) e pelos comandosgsd-tools state. - Corpo Markdown — seções legíveis por humanos cobrindo a posição atual, contexto acumulado, continuidade de sessão e métricas de desempenho.
O arquivo é intencionalmente pequeno (meta: menos de 100 linhas). Ele é um resumo do estado do projeto, não um arquivo histórico.
Frontmatter YAML
O frontmatter aparece entre delimitadores --- no início do arquivo. Todos os campos, exceto gsd_state_version e status, são opcionais; os campos podem estar ausentes quando seus dados ainda não estão disponíveis.
Exemplo comentado
---
gsd_state_version: '1.0'
milestone: v2.0
milestone_name: Code Quality
status: executing
# Campos de ciclo de vida de fase — todos opcionais (adicionados na v1.40.0, issue #2833)
active_phase: "4.5"
next_action: execute-phase
next_phases: ["4.5"]
progress:
total_phases: 17
completed_phases: 10
total_plans: 84
completed_plans: 47
percent: 59
# Campos adicionais escritos por syncStateFrontmatter
current_phase: "4"
current_phase_name: Observability
current_plan: "3"
last_updated: "2026-06-01T12:34:56.789Z"
state_head: 4f3c2b1a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b
last_activity: "2026-06-01"
stopped_at: "Phase 4 P3 execution complete"
paused_at: null
---
Referência de campos
| Campo | Tipo | Quando populado | Finalidade |
|---|---|---|---|
gsd_state_version |
string ('1.0') |
Sempre | Versão do esquema; escrito na primeira chamada state.* por syncStateFrontmatter. |
milestone |
string (ex.: v2.0) |
Quando um milestone está configurado | Versão do milestone atual, lida da configuração do projeto. |
milestone_name |
string | Quando um milestone está configurado | Rótulo legível do milestone (ex.: Code Quality). |
status |
string | Sempre | Estágio atual do ciclo de vida. Normalizado por normalizeStateStatus() — veja valores de status. |
active_phase |
string (ex.: "4.5") |
Um comando do orquestrador está em andamento nesta fase | O número da fase atualmente sendo processada. Definido como null entre fases. |
next_action |
string | Ocioso, com um comando recomendado | O slash command a executar a seguir: discuss-phase, plan-phase, execute-phase ou verify-phase. Definido como null quando um orquestrador está em andamento ou nenhuma recomendação está disponível. |
next_phases |
array YAML flow (ex.: ["4.5"]) |
Acompanha next_action |
Os IDs de fase aos quais o next_action se aplica (tipicamente 1–2 entradas). Definido como null nas mesmas condições que next_action. |
progress.total_phases |
inteiro | Quando dados de fase estão disponíveis | Número total de fases no milestone atual, derivado do ROADMAP.md e do diretório de fases. |
progress.completed_phases |
inteiro | Quando dados de fase estão disponíveis | Número de fases que têm todos os resumos de planos em disco (ou seja, todos os planos concluídos). |
progress.total_plans |
inteiro | Quando arquivos de plano existem | Soma de todos os arquivos de plano nas fases do milestone atual. |
progress.completed_plans |
inteiro | Quando arquivos de resumo existem | Soma dos resumos de planos concluídos (um SUMMARY.md por plano executado). |
progress.percent |
inteiro 0–100 | Quando dados de progresso estão disponíveis | Progresso do milestone na dimensão de fases (min(completed_plans/total_plans, completed_phases/total_phases)). A barra de progresso da linha de status é renderizada somente quando este campo está presente — sua ausência suprime a barra. |
current_phase |
string | Quando uma fase está em execução | Número da fase extraído do campo Current Phase: do corpo. |
current_phase_name |
string | Quando uma fase tem nome | Nome da fase extraído do campo Current Phase Name: do corpo. |
current_plan |
string | Quando um plano está em andamento | Número do plano extraído do campo Current Plan: do corpo. |
last_updated |
timestamp ISO-8601 | Sempre (na escrita) | Timestamp da última chamada a syncStateFrontmatter; escrito por realClock.nowIso(). |
state_head |
string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, or when the resolved repo is not the project's own — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
last_activity |
string | Quando definido no corpo | Data da última atividade, extraída do campo Last Activity: do corpo. |
last_activity_desc |
string | Quando definido no corpo | Descrição da última atividade, extraída do campo Last Activity Description: do corpo. |
stopped_at |
string | Quando um ponto de parada foi registrado | Descrição da última ação concluída; limitada à seção ## Session do corpo para evitar correspondência com prosa de arquivo. |
paused_at |
string | Quando o projeto está pausado | Descrição de forma livre do ponto de pausa; ausente ou null quando não pausado. |
Cardinalidade dos campos
| Campo | Cardinalidade |
|---|---|
gsd_state_version |
one |
milestone |
optional |
milestone_name |
optional |
current_phase |
optional |
current_phase_name |
optional |
current_plan |
optional |
status |
one |
stopped_at |
optional |
paused_at |
optional |
last_updated |
one |
last_activity |
optional |
last_activity_desc |
optional |
state_head |
optional |
progress.total_phases |
optional |
progress.completed_phases |
optional |
progress.total_plans |
optional |
progress.completed_plans |
optional |
progress.percent |
optional |
Valores de status
normalizeStateStatus() em gsd-core/bin/lib/state-document.cjs mapeia o texto bruto do corpo para estes valores canônicos:
| Valor canônico | Texto correspondente (sem diferenciação de maiúsculas/minúsculas) |
|---|---|
discussing |
contém discussing |
planning |
contém planning ou ready to plan |
executing |
contém executing, in progress ou ready to execute |
verifying |
contém verif |
completed |
contém complete ou done |
paused |
contém paused ou stopped, ou paused_at está presente |
unknown |
nenhuma das anteriores |
Quando um comando do orquestrador está em andamento, a convenção (issue #2833) é escrever o estágio do ciclo de vida diretamente em status:
| Comando | status durante a execução |
|---|---|
/gsd-discuss-phase |
discussing |
/gsd-plan-phase |
planning |
/gsd-execute-phase |
executing |
/gsd-verify-work |
verifying |
Ciclo de vida do status (ADR-2207)
The Status field follows a strict lifecycle across phase and milestone boundaries:
| Valor | Escrito por | Significado |
|---|---|---|
Ready to plan |
completePhaseCore (non-last phase) |
Next phase is ready for planning |
All phases complete |
completePhaseCore (last phase) |
All phases done; milestone awaiting formal close |
<version> milestone complete |
milestoneCompleteCore |
Milestone formally closed and archived |
Awaiting next milestone |
milestoneCompleteCore |
Terminal/archived state |
Phase-completion verbs never write Milestone complete (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
Cenas de renderização da linha de status
formatGsdState() em hooks/gsd-statusline.js lê o frontmatter analisado e emite a primeira cena correspondente. Se nenhum campo novo do ciclo de vida se aplicar, a renderização cai para o formato original byte a byte, inalterado desde a v1.38.x.
| Cena | Gatilho | Exemplo de exibição |
|---|---|---|
| 1. Fase ativa | active_phase está populado |
v2.0 [██░░░░░░░░] 20% · Phase 4.5 executing |
| 2. Ocioso, próximo recomendado | active_phase é null E tanto next_action quanto next_phases estão populados |
v2.0 [██░░░░░░░░] 20% · next execute-phase 4.5 |
| 3. Milestone completo | percent é 100 OU completed_phases == total_phases |
v2.0 [██████████] 100% · milestone complete |
| 4. Fallback padrão | Nenhuma das anteriores corresponde | v1.9 Code Quality · executing · ph 1/5 (formato existente) |
Prioridade de cena: quando active_phase e next_action estão populados, a Cena 1 prevalece — um orquestrador está em andamento, portanto uma "próxima recomendação" seria enganosa. Essa prioridade é imposta pela ordem de verificação em formatGsdState() e coberta pelo conjunto "scene priority" em tests/gsd-statusline.test.cjs.
A barra de progresso ([██░░░░░░░░] 20%) é anexada ao segmento do milestone somente quando progress.percent está presente no frontmatter; ausente significa sem barra.
Restrições de análise do frontmatter
O hook de linha de status usa análise baseada em regex (sem biblioteca YAML completa), portanto as seguintes restrições se aplicam. Elas são testadas em tests/gsd-statusline.test.cjs.
-
O frontmatter deve começar no primeiro caractere do arquivo. Qualquer coisa — incluindo comentários — acima do
---de abertura invalida a correspondência. A linha---de abertura deve ser exatamente isso, sem espaços no final. -
Comentários dentro de blocos aninhados não são suportados. O analisador do bloco
progress:requer que a próxima linha seja[ \t]+\w+:. Inserir um# commententreprogress:e sua primeira chave quebra a correspondência e a barra desaparece. Qualquer documentação pertence ao corpo doSTATE.md, não dentro dos blocos do frontmatter. -
O formato primário de
next_phasesé flow de linha única. O analisador tenta primeironext_phases: ["4.5", "4.6"]. Sequências em bloco (- 4.5\n- 4.6) também são analisadas, mas são menos confiáveis para renderização da linha de status. Prefira flow de linha única paranext_phasespara manter o analisador baseado em regex previsível. Se muitas fases candidatas precisarem ser registradas para fins de documentação, armazene-as no corpo doSTATE.md.
Se uma mudança futura substituir o analisador de regex por uma biblioteca YAML completa, essas restrições poderão ser relaxadas e os testes atualizados adequadamente.
Seções do corpo Markdown
O corpo (tudo após o --- de fechamento) segue o template em gsd-core/templates/state.md. As seções padrão são:
Referência do Projeto
Aponta para .planning/PROJECT.md. Contém:
- Valor central — a frase de uma linha da seção Core Value do
PROJECT.md. - Foco atual — qual fase está ativa.
Posição Atual
Onde o projeto está agora:
| Campo | Formato |
|---|---|
Phase: |
X of Y (Phase name) |
Plan: |
A of B in current phase |
Status: |
Texto livre, ex.: Ready to execute, Executing Phase 4, Phase complete — ready for verification |
Last activity: |
Data ISO (YYYY-MM-DD) quando escrito por handler; prosa narrativa quando elaborado pelo executor |
Progress: |
Barra visual, ex.: [████░░░░░░] 40% |
Todos os campos nesta seção têm valor único, e a seção é sobrescrita em vez de receber conteúdo anexado. Uma segunda linha Phase: não representa uma segunda posição nem um histórico — é uma entrada malformada. Os leitores não resolvem uma duplicata pela ordem no documento; eles a resolvem pela forma, verificada nesta ordem independentemente de onde cada forma apareça dentro da seção ## Current Position — negrito **Phase:** value em qualquer ponto da seção, depois texto simples Phase: value no início de uma linha, depois uma linha de tabela | Phase | value | — e apenas dentro da forma vencedora prevalece a primeira ocorrência. Uma linha em negrito ou em texto simples fora desta seção — uma entrada anterior em ## Archive, ou uma linha em negrito no frontmatter YAML — nunca é consultada; a implementação em produção sempre restringe o escopo à seção ## Current Position primeiro (#2956) antes de aplicar a hierarquia de formas. Duas consequências práticas: uma duplicata escrita em uma forma de prioridade mais alta vence mesmo que apareça por último na seção, de modo que uma linha em negrito acrescentada "para dar ênfase" sobrescreve silenciosamente uma linha em texto simples anterior em vez de ser ignorada; e uma linha Phase: indentada é invisível para a forma em texto simples (que se ancora no início da linha) e recai sobre a próxima forma que corresponder. A regra "a forma de prioridade mais alta vence" tem duas arestas afiadas: uma linha em negrito cujo valor é apenas espaço em branco final é resolvida como uma string vazia, em vez de recair sobre uma linha em texto simples válida abaixo dela; e uma linha de tabela cuja célula de rótulo está ela mesma em negrito (| **Phase:** | value |) é capturada primeiro pelo padrão em negrito, retornando o texto literal da célula, incluindo suas barras verticais. Escreva a seção substituindo-a, nunca adicionando uma linha.
O histórico de progresso não pertence aqui. Ele se acumula em ### Performance Metrics logo abaixo, que é a seção projetada para crescer.
Os campos Status: e Last activity: nesta seção são atualizados pelos handlers do GSD quando o valor existente é um padrão de template conhecido (invariante de Knuth: valores elaborados pelo executor são preservados). A lista completa de padrões de handler conhecidos está em KNOWN_TEMPLATE_DEFAULTS dentro de gsd-core/bin/lib/state-document.cjs.
Métricas de Desempenho
Rastreamento de velocidade de execução:
- Total de planos concluídos, duração média por plano.
- Tabela de detalhamento por fase (
Phase | Plans | Total | Avg/Plan). - Tendência recente: Improving / Stable / Degrading.
Atualizado após cada conclusão de plano.
Contexto Acumulado
Decisões — um resumo das decisões recentes que afetam o trabalho atual (o log completo vive em PROJECT.md). Adicionado via gsd-tools state add-decision.
Todos Pendentes — contagem e referência a .planning/todos/pending/. Capturado via /gsd-capture.
Bloqueadores/Preocupações — problemas que afetam trabalhos futuros, prefixados com a fase de origem. Adicionado via gsd-tools state add-blocker; resolvido via gsd-tools state resolve-blocker.
Continuidade de Sessão
Permite retomada instantânea de sessão:
Last session:— timestamp ISO-8601 da última sessão.Stopped at:— descrição da última ação concluída.Resume file:— caminho para um arquivo.continue-here*.mdse existir, caso contrárioNone.
Compatibilidade retroativa
Os campos de ciclo de vida de fase (active_phase, next_action, next_phases e progress.percent para a barra) são aditivos e opt-in por projeto:
- Um
STATE.mdsem nenhum dos campos de ciclo de vida populados é renderizado byte a byte de forma idêntica à v1.38.x e anteriores. - Adicionar qualquer campo de ciclo de vida é opt-in — o renderizador degrada graciosamente quando os campos estão ausentes.
- A barra de progresso é opt-in mesmo quando o bloco
progressexiste: somenteprogress.percentativa a barra;total_phasesecompleted_phasessozinhos não ativam.
O conjunto de testes formatGsdState #2833 backward compatibility em tests/gsd-statusline.test.cjs garante essa promessa; qualquer mudança que quebre a renderização legada do STATE.md fará o conjunto falhar.