* test(#2850): add failing-first tests for workstream statusline state readGsdState only ever reads the flat .planning/STATE.md via a directory walk-up; it has no path for .planning/workstreams/<ws>/STATE.md and never consults GSD_WORKSTREAM or the stored active-workstream pointer, so the GSD-state segment silently disappears in workstream mode. These tests prove the RED before the fix lands. Uses shared saveSessionEnv/restoreSessionEnv/clearSessionEnv helpers now added to tests/helpers.cjs (single source of truth for the session-env-var save/clear/restore pattern also used by tests/active-workstream-store.unit.test.cjs, which is updated here to consume the same shared helpers instead of its own local, already-diverged copy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2850): resolve active workstream in the statusline readGsdState only ever walked up looking for a flat .planning/STATE.md; it had no branch for .planning/workstreams/<ws>/STATE.md and never consulted GSD_WORKSTREAM or the stored active-workstream pointer, so the GSD-state segment silently vanished in workstream-mode projects with no root STATE.md (exit 0, no diagnostic). Reuses the existing CLI>env>store resolution seam (resolveActiveWorkstream, active-workstream-store.cts) and the existing mode-detection/path-building seam (listAvailableWorkstreams/planningPaths, planning-workspace.cts) rather than re-implementing either inline. When workstream mode is detected but nothing resolves, readGsdState now returns a {noActiveWorkstream:true} sentinel that formatGsdState/formatGsdStateCompact render as "no active workstream" -- observable, never silent emptiness. Flat-mode behavior and the case where a resolved workstream has no STATE.md yet are both unchanged. Adds active-workstream-store.cts's peekActiveWorkstream: a read-only sibling of getActiveWorkstream. resolveActiveWorkstream's default store lookup self-heals a stale/invalid pointer by deleting it (adapter.clear()) -- correct for a command, but not for a renderer invoked once per prompt, which must never mutate persistent, possibly cross-session state as a side effect of drawing a screen. The statusline now injects peekActiveWorkstream via resolveActiveWorkstream's own getStored override, keeping the env>store precedence itself fully reused while removing only the store tier's write side effect. This satisfies the issue's AC4 ("the fix is purely additive to what's displayed"). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2850): backfill changeset PR number to 3012 --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,17 @@ const childProcess = require('child_process');
|
||||
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
||||
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
||||
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
// #2850: reuse the existing workstream resolution seams rather than
|
||||
// re-implementing CLI>env>store precedence or path construction inline.
|
||||
// peekActiveWorkstream is the read-only sibling of the store-tier lookup
|
||||
// resolveActiveWorkstream defaults to (getActiveWorkstream) — that default
|
||||
// self-heals a stale/invalid pointer by deleting it, which is correct for a
|
||||
// command but not for a renderer invoked on every prompt. Injecting it via
|
||||
// resolveActiveWorkstream's own `getStored` override keeps the CLI>env>store
|
||||
// precedence itself fully reused (untouched); only the store tier's *write*
|
||||
// side effect is removed.
|
||||
const { resolveActiveWorkstream, peekActiveWorkstream } = require('../gsd-core/bin/lib/active-workstream-store.cjs');
|
||||
const { listAvailableWorkstreams, planningPaths } = require('../gsd-core/bin/lib/planning-workspace.cjs');
|
||||
|
||||
// --- Config + last-command readers ------------------------------------------
|
||||
|
||||
@@ -102,21 +113,65 @@ function readLastSlashCommand(transcriptPath) {
|
||||
// --- GSD state reader -------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Walk up from dir looking for .planning/STATE.md.
|
||||
* Returns parsed state object or null.
|
||||
* Read and parse a STATE.md if it exists. Returns the parsed state object,
|
||||
* `null` when the file is absent, or `null` on any read/parse failure (never
|
||||
* throws) — the single shared shape for both the flat and workstream reads
|
||||
* in readGsdState() below.
|
||||
*/
|
||||
function readStateFileOrNull(statePath) {
|
||||
if (!fs.existsSync(statePath)) return null;
|
||||
try {
|
||||
return parseStateMd(fs.readFileSync(statePath, 'utf8'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk up from dir looking for .planning/STATE.md (flat mode). If an ancestor
|
||||
* has no flat STATE.md but IS in workstream mode (.planning/workstreams/
|
||||
* present — the single-source-of-truth check `listAvailableWorkstreams`
|
||||
* shares with the init.progress/phase.complete #1912/#2028 guards, so this
|
||||
* can't drift from how every other GSD command detects the mode), resolve
|
||||
* the active workstream and read that workstream's STATE.md instead (#2850).
|
||||
*
|
||||
* Resolution reuses `resolveActiveWorkstream` (active-workstream-store.cjs)
|
||||
* called with an empty args array, so only its env>store precedence applies
|
||||
* here — the CLI leg is inert for this renderer, which never receives argv.
|
||||
* The store tier is `peekActiveWorkstream`, a READ-ONLY sibling of the
|
||||
* default `getActiveWorkstream`: the default self-heals a stale/invalid
|
||||
* pointer by deleting it, which is correct for a command but not for a
|
||||
* renderer invoked on every prompt — a render must never write or delete.
|
||||
*
|
||||
* Returns:
|
||||
* - the parsed state object when a flat or workstream STATE.md is found
|
||||
* - { noActiveWorkstream: true } when workstream mode is active at an
|
||||
* ancestor but no workstream can be resolved — an observable signal so
|
||||
* this is distinguishable from "GSD isn't installed here" (#2850)
|
||||
* - null when no .planning marker is found at all (GSD not present), or
|
||||
* when a workstream DOES resolve but its STATE.md doesn't exist yet
|
||||
* (negative space: mirrors flat-mode's own silent pre-STATE.md window)
|
||||
*/
|
||||
function readGsdState(dir) {
|
||||
const home = os.homedir();
|
||||
let current = dir;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = path.join(current, '.planning', 'STATE.md');
|
||||
if (fs.existsSync(candidate)) {
|
||||
const flatState = readStateFileOrNull(path.join(current, '.planning', 'STATE.md'));
|
||||
if (flatState !== null) return flatState;
|
||||
|
||||
if (listAvailableWorkstreams(current).length > 0) {
|
||||
let resolvedWs = null;
|
||||
try {
|
||||
return parseStateMd(fs.readFileSync(candidate, 'utf8'));
|
||||
resolvedWs = resolveActiveWorkstream(current, [], process.env, { getStored: peekActiveWorkstream }).ws;
|
||||
} catch (e) {
|
||||
return null;
|
||||
resolvedWs = null;
|
||||
}
|
||||
|
||||
if (!resolvedWs) return { noActiveWorkstream: true };
|
||||
|
||||
return readStateFileOrNull(planningPaths(current, resolvedWs).state);
|
||||
}
|
||||
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current || current === home) break;
|
||||
current = parent;
|
||||
@@ -217,6 +272,10 @@ function parseStateMd(content) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// #2850: shared literal for formatGsdState/formatGsdStateCompact's "nothing
|
||||
// resolvable" signal — one source of truth so the two renderers can't drift.
|
||||
const NO_ACTIVE_WORKSTREAM_LABEL = 'no active workstream';
|
||||
|
||||
/**
|
||||
* Render a 10-segment milestone progress bar (matches the context meter style).
|
||||
*
|
||||
@@ -249,6 +308,10 @@ function renderProgressBar(percent) {
|
||||
* progress.percent is present in frontmatter; absent → empty string.
|
||||
*/
|
||||
function formatGsdState(s) {
|
||||
// #2850: workstream mode with nothing resolvable — an observable signal,
|
||||
// never silent emptiness (distinguishes from "GSD isn't installed here").
|
||||
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
||||
|
||||
const parts = [];
|
||||
|
||||
// Milestone segment: version + name + (opt-in) progress bar
|
||||
@@ -364,6 +427,9 @@ function shortGsdStatus(status) {
|
||||
* The default "full" format is untouched.
|
||||
*/
|
||||
function formatGsdStateCompact(s) {
|
||||
// #2850: mirrors formatGsdState's observable "nothing resolvable" signal.
|
||||
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
||||
|
||||
const parts = [];
|
||||
|
||||
if (s.milestone) parts.push(s.milestone);
|
||||
|
||||
Reference in New Issue
Block a user