Skip to content

developer-experience: freshness stamp's --max-age silently freezes maxAgeDays:1 (issue #179) - #180

Draft
ruvnet wants to merge 2 commits into
mainfrom
dream/2026-10-13-developer-experience
Draft

ruvnet wants to merge 2 commits into
mainfrom
dream/2026-10-13-developer-experience

Conversation

@ruvnet

@ruvnet ruvnet commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Closes #179.

Hypothesis

Given dream-machine freshness stamp invoked with --max-age as a value-less flag (nothing after it, or another --flag immediately after, so parseArgs yields the boolean true) or with an out-of-range numeric string, the current code either (a) silently accepts maxAgeDays: 1 (the true case, since Number(true) === 1 is a valid positive safe integer) or (b) accepts any value Number() can parse and defers range checking to @dream-machine/witness's normalizedMaxAgeDays, producing a different exit code (2) and message shape than this file's own sibling flags (1). Replacing the inline Number(maxAgeRaw) with a parseMaxAgeDays() helper — same fail-closed shape as parseOpenCandidateCount/parseSinceRow, bounds mirroring MAX_EVIDENCE_AGE_DAYS from @dream-machine/witness — closes both at the CLI layer with one consistent usage error and exit code, with zero behavior change for any valid numeric --max-age. Confirmed.

Candidate

packages/cli/src/index.ts: adds parseMaxAgeDays(flag: string | boolean | undefined): number | undefined, mirroring the existing parseOpenCandidateCount/parseSinceRow pattern exactly (same file, few dozen lines above), with its upper bound sourced from @dream-machine/witness's already-exported MAX_EVIDENCE_AGE_DAYS constant rather than a duplicated literal. The freshness stamp handler's const maxAgeRaw = flags['max-age']; ...Number(maxAgeRaw) is replaced with a single call to the new helper.

packages/cli/src/index.test.ts: +3 tests — a valid-value regression guard, a value-less-flag rejection (the core finding), and a non-numeric/out-of-range table test (abc, 0, -1, 1.5, 3651).

+16/-2 in the source file, +34/-0 in the test file. 62 lines total, well under the 300-line target, one conceptual change.

Evaluation Receipt

Real evaluator npm test (vitest run && npm run test:governance), run live via git stash/git stash pop around the candidate diff:

vitest governance
Baseline (main@b2abcc1) 809 passed 195 passed
Candidate 812 passed (+3 new, 0 regressions) 195 passed (unchanged)

typecheck/lint clean.

Baseline

Measured live by stashing the candidate diff and re-running the identical npm test / npx vitest run commands — not inferred from logs, not simulated.

Darwin Lineage

DARWIN=not-applicable — a single validation-shape fix has no evolvable population or fitness landscape to search (same judgment recorded for #103/#122/#141/#160). Control-plane liveness check only: npx --yes @metaharness/darwin --version resolved and printed real usage output tonight; not required for this verdict.

Evidence

OBSERVATION (grep: --max-age not among the file's four already-hardened parse* helpers) → OBSERVATION (live repro, baseline: bare --max-age → maxAgeDays: 1, exit 0) → HYPOTHESIS (frozen above) → MEASUREMENT (809/195 → 812/195) → MEASUREMENT (live post-fix repro, all cases) → INFERENCE (independent critic, separate subagent context) → DECISION (adopt critic's upper-bound follow-up) → MEASUREMENT (re-run green) → DECISION (ship; ACCEPT).

Reward-Hack Check

Independent adversarial critic (separate subagent context, no authorship stake, read the real diff and the full surrounding file directly): CLEAR, with one non-blocking gap it found and this session adopted before shipping — the first-draft fix had no upper bound, so very large values sailed past the CLI guard and failed one call deeper inside the witness package with a different exit code/message shape. Fixed by importing MAX_EVIDENCE_AGE_DAYS and mirroring it in parseMaxAgeDays's range check. Critic confirmed: no existing assertion/fixture/golden value/threshold touched; the "valid --max-age" test is not tautological (drives the real run() → parseArgs → parseMaxAgeDays → evidenceFreshnessPolicyDigest → io.writeFile path, asserted against written JSON); whitespace/leading-zero/+/scientific-notation numeric strings parse sanely (matching this file's existing permissive-Number() convention elsewhere, not a new footgun); zero regression for any previously-valid input. Full re-run after the follow-up: npx vitest run packages/cli/src/index.test.ts → 104/104 passed.

Security Review

Pure in-memory CLI argument validation. No new I/O, network, filesystem, or credential surface. Strictly more restrictive than before — can only newly reject previously-silently-accepted input, never newly accept something previously rejected. The validated field gates @dream-machine/witness's evidence-freshness promotion check; a silently-accepted maxAgeDays: 1 never bypassed that gate, it only ever made it spuriously stricter — but it masked an operator mistake instead of surfacing it, which is the actual harm closed here.

Regression Analysis

0 pre-existing tests modified or removed. All 809 baseline vitest tests pass unchanged (812 total, +3 new); 195 governance tests unchanged. Lint/typecheck clean. Build clean, no wasm/NAPI degradation (pure-TS workspace).

ADR

None — a CLI input-validation bug fix matching an existing in-file precedent, not an architectural decision.

Gist

GIST=LOCAL — no gh CLI (GH_TOKEN invalid this session) or gist-creation MCP tool available. Full report committed at docs/dream-cycle/2026-10-13-developer-experience-report.md.

Issue

#179

Witness

report_sha256 : 0b13cd0acb559e75d0d01a2680f7f662775b87ac4e07183717ceb60a8ed74220
session_commit: b2abcc14a632d7e0948962065a7c6c8920cf306c
witness       : 9b1074fd31f798f311a39c8940cc6f446342b973acefa1559e8ef61a4c139058

Reproduce: node packages/cli/dist/bin.js witness verify-report docs/dream-cycle/2026-10-13-developer-experience-report.md --commit b2abcc14a632d7e0948962065a7c6c8920cf306c

Merge Policy

Human review required. This session never self-merges and never autonomously promotes candidate state. No automerge-safe label applied.


🤖 Generated with Claude Code

https://claude.ai/code/session_016d8nUtzH17Nd2EQqhL2h7f


Generated by Claude Code

…roze maxAgeDays:1 (issue TBD)

packages/cli/src/index.ts's `freshness stamp` command computed Number(flag)
directly on --max-age. parseArgs turns a value-less flag into the boolean
true, and Number(true) === 1 is a valid positive safe integer, so a bare
--max-age silently wrote the strictest possible evidence-freshness policy
(maxAgeDays: 1) instead of erroring like the file's already-hardened
--open-count/--since-row flags.

Adds parseMaxAgeDays() mirroring that existing precedent, plus an upper
bound (MAX_EVIDENCE_AGE_DAYS, imported from @dream-machine/witness) found
by an independent critic review so out-of-range values fail at a single
CLI-level exit code instead of leaking one layer deeper into the witness
package's own differently-shaped error.

npm test: 809+195 -> 812+195 (+3 new, 0 regressions). typecheck/lint clean.

Rotated tonight's DEEP surface from SLOT 3 (security-adversarial) to SLOT 4
(developer-experience) per the ledger's own learning signal: the
supplychain.ts unpinned-npx detector has had 5 narrow bypass-widening PRs
across prior nights (#68/#99/#138/#149/#157), none merged -- repeated >=3x,
so not re-widened a 6th time tonight. Checked issue #160/PR #161 (open,
unreviewed) in full first: it hardens 9 different (flags.X as string) ||
fallback call sites and does not touch --max-age's distinct Number()-
coercion shape, so this is not a duplicate.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016d8nUtzH17Nd2EQqhL2h7f
Ledger append ran before the issue/PR existed (chicken-and-egg: the ledger
row includes the PR number, but the PR includes the committed ledger row).
Issue #179 and PR #180 now exist; replacing the LOCAL/LOCAL placeholders.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016d8nUtzH17Nd2EQqhL2h7f

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants