* enhance(#2573): stamp STATE.md with its commit and surface a commit-age freshness hint Adds a `state_head` stamp to STATE.md and derives a tri-state commit-age freshness proxy (state_commits_behind / state_commit_stale) through state.cjs's readStateHeadFreshness, surfaced on smart-entry signals and as health W024. The proxy is advisory: classify() deliberately does NOT consume it (ADR-1787 locks the classification/routing boundary — a signal, not a route). Composes with #3099 and #1882 (both merged to next after this branch): the commit-age proxy reads `state_head` while the LAST_ACTIVITY_UNPARSEABLE diagnostic reads `last_activity` — two different fields, not "two staleness signals on one field." A new regression test asserts a STATE.md carrying both an unparseable last_activity AND a valid state_head resolves each independently (diagnostic fires once; freshness reads state_head, commits_behind 0). Rebased onto next (flattened): resolved the add/add conflicts in src/smart-entry.cts (kept both the #2573 freshness import/derivation and the #3099 diagnostic import/call) and tests/smart-entry.unit.test.cjs (kept both describe blocks). Drift-ack for health.md's W024 row is unchanged (12348 B). Tests: smart-entry 62, state/state-transition/health/verify 639, all pass. * chore(#2573): allowlist health-validation test in the prompt-injection scan The scanner's `exec('` code-execution pattern matches the benign `re.exec('<phase-id>')` RegExp method calls in the phase-ID grammar tests (pre-existing: 16 such calls on next, this PR adds none). The file entered the diff-mode scan's changed-file set only because #2573's W024 state_head assertions touch it. Allowlist it alongside the other test files that carry pattern-matching content as data (same DEFECT.PROMPT-INJECTION-SCAN-COLLISION class). Scanner self-test 38/0; diff scan 14 files, 0 findings.
This commit is contained in:
@@ -52,6 +52,9 @@ const { stateFieldValue } = stateDocument;
|
||||
import phaseId = require('./phase-id.cjs');
|
||||
const { comparePhaseNum, extractPhaseToken, normalizePhaseName, phaseTokenMatches } = phaseId;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import stateMod = require('./state.cjs');
|
||||
const { readStateHeadFreshness } = stateMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import unusableInput = require('./unusable-input.cjs');
|
||||
const { warnUnusableInput, UNUSABLE_REASON } = unusableInput;
|
||||
|
||||
@@ -104,6 +107,18 @@ export interface SmartEntrySignals {
|
||||
*/
|
||||
roadmap_total_phases: number | null;
|
||||
roadmap_completed_phases: number | null;
|
||||
/**
|
||||
* Commits between STATE.md's recorded `state_head` and HEAD (#2573). Null
|
||||
* when unknown — no stamp, no git, or an unresolvable commit.
|
||||
*/
|
||||
state_commits_behind: number | null;
|
||||
/**
|
||||
* Tri-state freshness proxy: null = unknown, false = written at HEAD,
|
||||
* true = the codebase has moved since STATE.md was written. Advisory only —
|
||||
* classify() deliberately does NOT consume this (ADR-1787 locks the
|
||||
* classification/routing boundary; this is a signal, not a route).
|
||||
*/
|
||||
state_commit_stale: boolean | null;
|
||||
}
|
||||
|
||||
export interface SmartEntryResult {
|
||||
@@ -304,6 +319,9 @@ export function detectSignals(cwd: string, now: () => number = Date.now): SmartE
|
||||
stale_activity: false,
|
||||
roadmap_total_phases: null,
|
||||
roadmap_completed_phases: null,
|
||||
// No STATE.md (or unreadable) → no stamp to compare. Unknown, not fresh.
|
||||
state_commits_behind: null,
|
||||
state_commit_stale: null,
|
||||
};
|
||||
if (!hasPlanning) return empty;
|
||||
|
||||
@@ -395,6 +413,12 @@ export function detectSignals(cwd: string, now: () => number = Date.now): SmartE
|
||||
}
|
||||
}
|
||||
|
||||
// #2573: commit-age freshness proxy. Derived through state.cjs's
|
||||
// readStateHeadFreshness so the tri-state and the hash fence stay identical
|
||||
// to validate.health's W024 — one derivation, two surfaces.
|
||||
const stateHeadRaw = stateFieldValue(fm, body, 'state_head', 'State Head').value;
|
||||
const freshness = readStateHeadFreshness(cwd, stateHeadRaw);
|
||||
|
||||
return {
|
||||
current_phase: parseIntOrNull(currentPhaseRaw),
|
||||
total_phases: parseIntOrNull(totalPhasesRaw),
|
||||
@@ -411,6 +435,8 @@ export function detectSignals(cwd: string, now: () => number = Date.now): SmartE
|
||||
stale_activity: staleActivity,
|
||||
roadmap_total_phases: roadmapTotalPhases,
|
||||
roadmap_completed_phases: roadmapCompletedPhases,
|
||||
state_commits_behind: freshness.commits_behind,
|
||||
state_commit_stale: freshness.commit_stale,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,11 @@ export const FIELD_CLASSIFICATION: Readonly<Record<string, FieldClassification>>
|
||||
last_activity: { source: 'body', preservation: 'derive' } as FieldClassification, // always refresh on transition
|
||||
last_activity_desc: { source: 'body', preservation: 'preserve-when-unchanged' } as FieldClassification,
|
||||
|
||||
// Commit provenance (#2573) — ambient git read, recomputed on every write,
|
||||
// exactly like last_updated. Never preserved: a stale stamp would claim
|
||||
// STATE.md was written against a commit it wasn't.
|
||||
state_head: { source: 'free', preservation: 'derive' } as FieldClassification, // #2573
|
||||
|
||||
// Progress block (disk-derived, except the curated progress ratchet)
|
||||
progress: { source: 'curated', preservation: 'preserve-always' } as FieldClassification, // #3242, #1446
|
||||
'progress.total_phases': { source: 'disk', preservation: 'derive' } as FieldClassification,
|
||||
|
||||
218
src/state.cts
218
src/state.cts
@@ -20,7 +20,7 @@ const { escapeRegex, parsePhaseFromProse, PHASE_NUMBER_TOKEN_SOURCE, phaseKeyFro
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import roadmapParserMod = require('./roadmap-parser.cjs');
|
||||
const { getMilestoneInfo, extractCurrentMilestone, isMilestoneBoundedInRoadmap, hasMilestoneSectioning } = roadmapParserMod;
|
||||
import { platformWriteSync, platformReadSync, platformEnsureDir, retryRenameSync, toPosixPath } from './shell-command-projection.cjs';
|
||||
import { platformWriteSync, platformReadSync, platformEnsureDir, retryRenameSync, toPosixPath, execGit } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
const { planningDir, planningPaths } = planningWorkspace;
|
||||
@@ -42,6 +42,10 @@ import phaseLocatorMod = require('./phase-locator.cjs');
|
||||
const { listMilestonePhaseDirs } = phaseLocatorMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import stateTransitionMod = require('./state-transition.cjs');
|
||||
|
||||
// #2573 D5: used to pin `git rev-parse` to the project's own repo. Imports only
|
||||
// node builtins, so it introduces no cycle on this path.
|
||||
import { findProjectRoot } from './project-root.cjs';
|
||||
const { transitionCore, applyStatePreservation, sliceCurrentPositionSection } = stateTransitionMod;
|
||||
type StateTransitionIntent = stateTransitionMod.StateTransitionIntent;
|
||||
type StateTransitionDeps = stateTransitionMod.StateTransitionDeps;
|
||||
@@ -1971,6 +1975,12 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined, sto
|
||||
fm['last_updated'] = realClock.nowIso();
|
||||
if (lastActivity) fm['last_activity'] = lastActivity;
|
||||
if (lastActivityDesc) fm['last_activity_desc'] = lastActivityDesc;
|
||||
// #2573: stamp the commit this STATE.md was written against, so consumers can
|
||||
// report how far the codebase has moved since. Omitted entirely outside a git
|
||||
// repo — an absent field reads as "unknown", which is the honest answer and
|
||||
// keeps every consumer's tri-state intact (see readStateHeadFreshness).
|
||||
const stateHead = readGitHeadSha(cwd);
|
||||
if (stateHead) fm['state_head'] = stateHead;
|
||||
|
||||
const progress: Record<string, unknown> = {};
|
||||
if (totalPhases !== null) progress['total_phases'] = totalPhases;
|
||||
@@ -1983,6 +1993,186 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined, sto
|
||||
return fm;
|
||||
}
|
||||
|
||||
// ─── state_head commit provenance (#2573) ────────────────────────────────────
|
||||
//
|
||||
// STATE.md records the commit it was written against (`state_head`); consumers
|
||||
// derive how many commits the codebase has moved since. This mirrors the shipped
|
||||
// graphify commit-staleness contract (src/graphify.cts, #3170) rather than
|
||||
// inventing a second vocabulary: `commits_behind` is a count, and `commit_stale`
|
||||
// is TRI-STATE — null means "we don't know" (no git, no stamp, unresolvable
|
||||
// commit), which is deliberately distinct from false ("known fresh").
|
||||
//
|
||||
// IMPORTANT — this is a freshness PROXY, never a drift measurement.
|
||||
// `rev-list state_head..HEAD` counts every commit in between, including ones
|
||||
// that never touched anything STATE.md describes. And because `state_head`
|
||||
// restamps on EVERY state write, a low count means "something wrote STATE
|
||||
// recently", NOT "STATE's content is accurate". Consumers must word it as
|
||||
// approximate and must never gate on it.
|
||||
|
||||
/** Strict hash fence before any value from disk reaches a git argument. */
|
||||
const STATE_HEAD_HASH_RE = /^[0-9a-f]{4,40}$/i;
|
||||
|
||||
/**
|
||||
* Resolve the project's current HEAD sha, or null when unavailable.
|
||||
* Bounded + non-interactive via execGit (10s timeout, GIT_TERMINAL_PROMPT=0);
|
||||
* a non-repo, missing git, or timeout degrades to null rather than throwing.
|
||||
*/
|
||||
/**
|
||||
* Does the project root carry its own git repository?
|
||||
*
|
||||
* #2573 D5. `git rev-parse HEAD` walks UP from cwd and stops at the FIRST
|
||||
* enclosing `.git`. So the repo that answered is the project's own exactly when
|
||||
* the project root itself carries a `.git` entry — a directory for a normal
|
||||
* clone, a file for a worktree or submodule, both of which `existsSync` accepts.
|
||||
* If it does not, the answer necessarily came from an ancestor repo and the
|
||||
* stamp would assert provenance the project cannot claim.
|
||||
*
|
||||
* Deliberately a filesystem-identity check rather than comparing
|
||||
* `--show-toplevel` against the project root as strings. That comparison is
|
||||
* unreliable across platforms — macOS resolves temp dirs through
|
||||
* `/private/var/…`, Windows adds 8.3 short names and separator/case variance —
|
||||
* and an over-strict compare degrades healthy projects to "unknown", which is
|
||||
* the very failure this check exists to prevent, inverted. No path spelling is
|
||||
* involved here at all.
|
||||
*/
|
||||
function projectOwnsItsRepo(projectRoot: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(path.join(projectRoot, '.git'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readGitHeadSha(cwd: string | undefined): string | null {
|
||||
if (!cwd) return null;
|
||||
// #2573 degrade path D5. `git rev-parse HEAD` walks UP from cwd to the nearest
|
||||
// enclosing `.git`, and nothing pins that repo to the project. A GSD project
|
||||
// living inside an unrelated checkout — a dotfiles/notes repo, or the outer
|
||||
// workspace of a `planning.sub_repos` layout where all code commits land in
|
||||
// the sub-repos — would otherwise measure freshness against a repo it has no
|
||||
// relationship to, and report `commit_stale: false` ("known fresh") while
|
||||
// doing it. Unverified provenance must degrade to unknown, never to fresh.
|
||||
//
|
||||
// TWO independent conditions must hold before a stamp is trustworthy, and both
|
||||
// are checked below because either alone is insufficient:
|
||||
// 1. the project root owns a `.git` (else an ancestor repo answered), and
|
||||
// 2. the project is not a `sub_repos` workspace (else the repo that answers
|
||||
// is the outer wrapper, whose HEAD does not move when the code does).
|
||||
// KNOWN LIMITATION, by design: in a `sub_repos` workspace this feature reports
|
||||
// unknown rather than measuring the children. Per-child freshness needs a
|
||||
// defined aggregate across N histories and is out of scope for this increment.
|
||||
//
|
||||
// `--show-toplevel HEAD` answers both in ONE spawn, so pinning costs no extra
|
||||
// subprocess on this path (the caller holds the STATE lock).
|
||||
let projectRoot: string;
|
||||
try {
|
||||
projectRoot = findProjectRoot(cwd);
|
||||
} catch {
|
||||
return null; // cannot prove which repo would answer → unknown
|
||||
}
|
||||
if (!projectOwnsItsRepo(projectRoot)) return null;
|
||||
|
||||
// #2573 D5, sub_repos flavor. Owning a `.git` is necessary but NOT sufficient.
|
||||
// In a `planning.sub_repos` workspace the outer directory can legitimately own
|
||||
// BOTH `.planning/` and its own repo while every code commit lands in a nested
|
||||
// child repo — `docs/CONFIGURATION.md` describes sub_repos as scoping work per
|
||||
// sub-repo "instead of treating the outer repo as a monorepo". The outer HEAD
|
||||
// then never advances, so `merge-base --is-ancestor` passes trivially and
|
||||
// `rev-list` counts 0: the stamp would report `commit_stale: false`, i.e.
|
||||
// "known fresh", while the code it describes has moved arbitrarily far.
|
||||
//
|
||||
// That is a WRONG answer, not a missing one, and it is the same invariant the
|
||||
// ancestor-repo check above exists to protect: a freshness claim the project
|
||||
// cannot substantiate must degrade to unknown, never to fresh. Measuring the
|
||||
// children instead would mean picking one HEAD out of N unrelated histories
|
||||
// (or inventing an aggregate), which is a design question beyond this
|
||||
// increment — so this scopes to the honest tri-state and declines to answer.
|
||||
// Deliberately keyed on the DECLARED config rather than probing the filesystem
|
||||
// for nested `.git` entries: the declaration is what the workspace asserts
|
||||
// about itself, and a probe would spuriously fire on a vendored dependency.
|
||||
try {
|
||||
const subRepos = (loadConfig(projectRoot) as { sub_repos?: unknown }).sub_repos;
|
||||
if (Array.isArray(subRepos) && subRepos.length > 0) return null;
|
||||
} catch {
|
||||
return null; // cannot read the layout → cannot claim provenance → unknown
|
||||
}
|
||||
|
||||
const r = execGit(['rev-parse', 'HEAD'], { cwd });
|
||||
if (r.exitCode !== 0) return null;
|
||||
|
||||
const sha = r.stdout.trim();
|
||||
return STATE_HEAD_HASH_RE.test(sha) ? sha : null;
|
||||
}
|
||||
|
||||
interface StateHeadFreshness {
|
||||
/** The recorded stamp, short form, or null when absent/malformed. */
|
||||
state_head: string | null;
|
||||
/** Current HEAD, short form, or null outside a resolvable repo. */
|
||||
current_commit: string | null;
|
||||
/** Commits between the stamp and HEAD; null when either end is unknown. */
|
||||
commits_behind: number | null;
|
||||
/** Tri-state: null = unknown, false = known fresh, true = moved since. */
|
||||
commit_stale: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the commit-age freshness signal from a recorded `state_head`.
|
||||
*
|
||||
* Single source of truth for the derivation — `validate.health` (W024) and
|
||||
* smart-entry both consume this rather than re-deriving it, so the tri-state
|
||||
* and the hash fence cannot drift apart between surfaces.
|
||||
*
|
||||
* Never throws: every unresolvable input degrades to nulls.
|
||||
*/
|
||||
function readStateHeadFreshness(
|
||||
cwd: string | undefined,
|
||||
stateHead: unknown,
|
||||
): StateHeadFreshness {
|
||||
const raw = (typeof stateHead === 'string' ? stateHead : '').trim();
|
||||
const stamp = STATE_HEAD_HASH_RE.test(raw) ? raw : null;
|
||||
const head = readGitHeadSha(cwd);
|
||||
|
||||
let commitsBehind: number | null = null;
|
||||
let commitStale: boolean | null = null;
|
||||
if (stamp && head && cwd) {
|
||||
// The stamp must be an ANCESTOR of HEAD before a distance means anything.
|
||||
// `rev-list --count A..B` exits 0 with "0" when A is not reachable from B —
|
||||
// which is what a `reset --hard` to an earlier commit, a rebase or squash
|
||||
// that drops the stamped commit, or a force-push rewriting history all
|
||||
// produce. Without this guard those cases report `commit_stale: false`,
|
||||
// i.e. "known fresh", for a codebase that was actually rewound past the
|
||||
// stamp — collapsing the exact unknown-vs-fresh distinction this tri-state
|
||||
// exists to preserve. A non-ancestor stamp is UNKNOWN, so it stays null.
|
||||
const ancestry = execGit(['merge-base', '--is-ancestor', stamp, head], { cwd });
|
||||
if (ancestry.exitCode === 0) {
|
||||
const r = execGit(['rev-list', '--count', `${stamp}..${head}`], { cwd });
|
||||
if (r.exitCode === 0) {
|
||||
const n = parseInt(r.stdout.trim(), 10);
|
||||
if (Number.isFinite(n)) {
|
||||
commitsBehind = n;
|
||||
// #2573 D4 — deliberately RAW, not thresholded. `commit_stale` means
|
||||
// exactly what its contract says: the codebase has moved since the
|
||||
// stamp. Applying an advisory threshold here would make the field lie
|
||||
// at n < threshold, and W024 needs the true count to threshold on.
|
||||
// Alarm-fatigue is handled at the ALARMING surface, not the
|
||||
// derivation: W024 (the only user-visible consumer) fires at
|
||||
// STATE_HEAD_ADVISORY_COMMITS, which absorbs the `commit_docs: true`
|
||||
// off-by-one. Smart-entry re-exports the raw tri-state as advisory
|
||||
// JSON and is not consumed by classify().
|
||||
commitStale = n > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state_head: stamp ? stamp.slice(0, 7) : null,
|
||||
current_commit: head ? head.slice(0, 7) : null,
|
||||
commits_behind: commitsBehind,
|
||||
commit_stale: commitStale,
|
||||
};
|
||||
}
|
||||
|
||||
function syncStateFrontmatter(content: string, cwd: string | undefined, authoritativeFm?: Record<string, unknown>): string {
|
||||
// Read existing frontmatter BEFORE stripping — it may contain values
|
||||
// that the body no longer has (e.g., Status field removed by an agent).
|
||||
@@ -2081,9 +2271,28 @@ function syncStateFrontmatter(content: string, cwd: string | undefined, authorit
|
||||
// Schema-owned keys (already in derivedFm from buildStateFrontmatter + the
|
||||
// preserve guards above) still win.
|
||||
for (const key of Object.keys(existingFm)) {
|
||||
if (!(key in derivedFm) && existingFm[key] !== undefined) {
|
||||
derivedFm[key] = existingFm[key];
|
||||
}
|
||||
if (key in derivedFm || existingFm[key] === undefined) continue;
|
||||
|
||||
// #2573: a `source: 'free'` field is the writer's word on every write and
|
||||
// carries no preservation (see the FieldSource doc). When buildStateFrontmatter
|
||||
// omits it — `state_head` outside a git repo, per its `if (stateHead)` guard —
|
||||
// carrying the old value forward would re-assert provenance the file no longer
|
||||
// has: a stale state_head would claim STATE.md was written against a commit it
|
||||
// wasn't, contradicting its own ADR-1769 row.
|
||||
//
|
||||
// Narrow the skip to `source: 'free'`, NOT every `derive` row. `last_activity`
|
||||
// ({source:'body'}) and the `progress.*` rows ({source:'disk'}) are also
|
||||
// `derive`, but they are body/disk-sourced and MUST still carry forward when
|
||||
// the writer omits them this pass — dropping `last_activity` here is silent
|
||||
// frontmatter data loss and would defeat #2570's staleness fix downstream.
|
||||
// `last_updated` and `gsd_state_version` are the only other `free` rows and are
|
||||
// both produced unconditionally by buildStateFrontmatter, so this loop never
|
||||
// reaches them; `state_head` is the sole field the skip governs. Consult the
|
||||
// table rather than naming fields, so the policy stays single-sourced.
|
||||
const classification = stateTransitionMod.getFieldClassification(key);
|
||||
if (classification && classification.source === 'free') continue;
|
||||
|
||||
derivedFm[key] = existingFm[key];
|
||||
}
|
||||
|
||||
// #2567: guard the information-losing direction — a stale archive
|
||||
@@ -3734,6 +3943,7 @@ export = {
|
||||
writeStateMd,
|
||||
readModifyWriteStateMd,
|
||||
syncStateFrontmatter,
|
||||
readStateHeadFreshness,
|
||||
withStateLock,
|
||||
updatePerformanceMetricsSection,
|
||||
cmdStateLoad,
|
||||
|
||||
@@ -62,7 +62,18 @@ const { determinePhaseStatus } = commandsMod;
|
||||
|
||||
const { planningDir, planningRoot } = planningWorkspace;
|
||||
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
||||
const { writeStateMd } = stateMod;
|
||||
const { writeStateMd, readStateHeadFreshness } = stateMod;
|
||||
|
||||
/**
|
||||
* W024 (#2573) threshold — how many commits STATE.md may lag HEAD before
|
||||
* `validate.health` mentions it.
|
||||
*
|
||||
* Deliberately coarse. `state_head` restamps on every state write, so a small
|
||||
* count is normal for any active project; firing near zero would make health
|
||||
* noisy for healthy projects without telling anyone anything. This is a
|
||||
* freshness proxy, not a drift measurement — see readStateHeadFreshness.
|
||||
*/
|
||||
const STATE_HEAD_ADVISORY_COMMITS = 20;
|
||||
const { MODEL_PROFILES } = modelProfilesMod;
|
||||
|
||||
// Unused but imported for structural parity
|
||||
@@ -1642,6 +1653,29 @@ function cmdValidateHealth(
|
||||
repairs.push('regenerateState');
|
||||
} else {
|
||||
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
// W024 (#2573): STATE.md commit-age freshness. Advisory ONLY — it appends
|
||||
// to warnings[] and never touches `status`, the repair set, or any existing
|
||||
// count. Silent when the stamp is absent or unresolvable: "unknown" is not
|
||||
// a finding. The threshold is deliberately coarse so an ordinary project
|
||||
// stays quiet — firing on every project would change health's observable
|
||||
// "clean" state for anything gating on it.
|
||||
{
|
||||
const fm = extractFrontmatter(stateContent) as Record<string, unknown>;
|
||||
const freshness = readStateHeadFreshness(cwd, fm['state_head']);
|
||||
if (
|
||||
freshness.commits_behind !== null &&
|
||||
freshness.commits_behind >= STATE_HEAD_ADVISORY_COMMITS
|
||||
) {
|
||||
addIssue(
|
||||
'warning',
|
||||
'W024',
|
||||
`STATE.md was written ${freshness.commits_behind} commits ago (at ${freshness.state_head}) — treat its contents as approximate`,
|
||||
'Re-read the current phase artifacts before relying on STATE.md, or run a GSD command that refreshes it',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const phaseRefs = [
|
||||
...stateContent.matchAll(new RegExp(`[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g')),
|
||||
].map(
|
||||
@@ -2739,6 +2773,7 @@ export = {
|
||||
cmdValidateAgents,
|
||||
cmdVerifySchemaDrift,
|
||||
cmdVerifyCodebaseDrift,
|
||||
STATE_HEAD_ADVISORY_COMMITS,
|
||||
// Test seam (#1883): listMilestoneArchiveDirs is private and exercised through
|
||||
// the validate command, which runs in a subprocess — an fs monkeypatch in the
|
||||
// test process cannot reach it. Exposed under a leading underscore so the
|
||||
|
||||
Reference in New Issue
Block a user