* 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user