* 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:
@@ -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