* fix(#2444): re-resolve body-parser to 2.3.0 in lockfile (GHSA-v422-hmwv-36x6) GHSA-v422-hmwv-36x6 (body-parser DoS via invalid limit value, low severity, published 2026-07-20T23:23:26Z) made tests/npm-integrity-gate.test.cjs (#3588: root workspace production tree has no advisories) fail any subsequent npm audit --omit=dev. The advisory affects body-parser >=2.0.0 <2.3.0 pulled transitively via @anthropic-ai/claude-agent-sdk -> @modelcontextprotocol/sdk -> express -> body-parser@2.2.2. express@5.2.1 already declares body-parser as ^2.2.1, so 2.3.0 is a valid re-resolution within express's own compatibility range — no override needed. Regenerated the lockfile via 'npm audit fix --omit=dev' which re-resolves transitive deps within their declared ranges; package.json is unchanged. Verified: npm audit --omit=dev reports 0/0/0/0/0 advisories; body-parser now reads as 2.3.0 in 'npm ls body-parser --omit=dev'. * test(#2450): failing-first CRLF regression for record-session insert path cmdStateRecordSession's section-rewrite regexes (src/state.cts:1166, :1195) used literal \\n which cannot match CRLF STATE.md delimiters. The detector regex (CRLF-tolerant via $ under /m) entered the rewrite branch, the writer regex silently no-op'd, but updated.push(...)/ sessionCreated=true ran unconditionally. Result: caller reported recorded:true with 'Resume File' in updated, but the field was never written to disk. With core.autocrlf=input, the CRLF working-tree file produces no git diff, so the bug was invisible. Adds three regression tests covering all three rewrite paths: - CRLF STATE.md with ## Session and Resume file absent - CRLF STATE.md with ## Session and Stopped at absent - CRLF STATE.md with ## Session Continuity (bootstrap shape) Each asserts the field IS on disk (the bug discriminator: the pre-fix command's JSON output looked identical to a successful write). * fix(#2450): CRLF-tolerant session-section rewrite + no-op-detection guard Two regexes in cmdStateRecordSession used literal \\n which cannot match CRLF STATE.md, silently no-op'ing the section rewrite while the CRLF- tolerant detector above entered the branch. The reporter's exact repro: on a CRLF STATE.md with one canonical session field absent, the command returned recorded:true + updated:['Resume File'] but the field was never written to disk. Three changes: 1. src/state.cts:1166 (canonical ## Session rewrite regex): \\n -> \\r?\\n 2. src/state.cts:1195 (## Session Continuity insert regex): \\n -> \\r?\\n 3. Defensive invariant (#2450 class fix per reporter's suggestion): track whether the chosen branch's replace actually matched via callback flag. Only set sessionCreated=true and push to updated when rewriteMatched. Unreachable post-fix, but fail-loud is the right posture for a silent- success gate. If a future drift between the detector and writer regexes reintroduces the asymmetry, the caller will not see false updated entries. Same canonical CRLF-tolerant form already in use at check-command-router.cts :205 (extractPlanDesignatedSections). Same bug class previously fixed in #1658, #1668, #2206, #2449. * fix(#2450): address review followups + add changeset Code-review + security-review both flagged the unreachable else at the Session Continuity branch (defaulted rewriteMatched=true in dead code, re-arming the bug class for future drift). Removed the else; the remaining code path leaves rewriteMatched=false if linesToInsert is empty, preserving the fail-loud posture. Added scope-limitation doc to the rewriteMatched gate: it covers the INSERT path only, not the earlier in-place stateReplaceField successes (which DID land on disk and correctly push to updated unconditionally). Tests: - Normalized STATE_CRLF_SESSION_MISSING_RESUME fixture to match the canonical 6-key frontmatter of STATE_WITH_SESSION (code-review I2). - Added mixed-ending test (LF frontmatter + CRLF body) to close CONTRIBUTING.md:490 'Mixed CRLF/LF newlines' requirement (I1). Added Fixed changeset (code-review H1). * docs(changeset): backfill PR number to 2482
This commit is contained in:
@@ -4806,6 +4806,168 @@ describe('T6 section-splice characterization — record-session', () => {
|
||||
cleanup(d);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── #2450: CRLF STATE.md must not silently drop inserted session fields ─────
|
||||
//
|
||||
// Bug class: cmdStateRecordSession's section-rewrite regex used literal \n
|
||||
// for the `## Session` and `## Session Continuity` heading boundaries, which
|
||||
// cannot match a CRLF STATE.md (---\r\n style). The detector regex (using
|
||||
// /^## Session[ \t]*$/im) WAS CRLF-tolerant ($ under /m treats \r as a line
|
||||
// terminator), so the bug fired when a canonical session field was missing
|
||||
// and had to be INSERTED via the section-rewrite path: the detector entered
|
||||
// the branch, the writer regex silently no-op'd, but `updated.push('Resume File')`
|
||||
// ran unconditionally → reported as updated but never written to disk.
|
||||
//
|
||||
// With core.autocrlf=input, the CRLF working-tree file produces no git diff,
|
||||
// so the contributor cannot tell their STATE.md was misclassified.
|
||||
|
||||
const STATE_CRLF_SESSION_MISSING_RESUME = [
|
||||
'---',
|
||||
"gsd_state_version: '1.0'",
|
||||
'milestone: v1.0',
|
||||
'milestone_name: TestMilestone',
|
||||
'status: executing',
|
||||
"last_updated: '2026-01-01T00:00:00.000Z'",
|
||||
"last_activity: '2026-01-01'",
|
||||
'---',
|
||||
'',
|
||||
'# Project State',
|
||||
'',
|
||||
'## Session',
|
||||
'',
|
||||
'**Last session:** 2026-01-01T00:00:00.000Z',
|
||||
'**Stopped at:** None',
|
||||
// Resume file absent — forces the section-rewrite insert path (the buggy block)
|
||||
'',
|
||||
].join('\r\n');
|
||||
|
||||
test('record-session --resume-file on CRLF STATE.md with field absent: field IS written (#2450)', () => {
|
||||
const d = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(path.join(d, '.planning', 'STATE.md'), STATE_CRLF_SESSION_MISSING_RESUME);
|
||||
const result = runGsdTools(['state', 'record-session', '--resume-file', 'plan-3.md'], d);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
// Bug discriminator: command reported recorded:true + 'Resume File' in
|
||||
// updated, but the field was never written to disk. The disk assertion
|
||||
// below is what fails pre-fix and passes post-fix.
|
||||
assert.ok(Array.isArray(output.updated) && output.updated.includes('Resume File'),
|
||||
`updated must include 'Resume File': ${JSON.stringify(output.updated)}`);
|
||||
|
||||
const after = fs.readFileSync(path.join(d, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(
|
||||
/\*\*Resume file:\*\*\s*plan-3\.md/.test(after),
|
||||
`Resume file field must be on disk after the call; STATE.md head:\n${after.slice(0, 500)}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(d);
|
||||
}
|
||||
});
|
||||
|
||||
test('record-session --stopped-at on CRLF STATE.md with field absent: field IS written (#2450)', () => {
|
||||
// Same bug class, different field. The bug fires whenever a canonical
|
||||
// session field must be INSERTED (vs. same-line replaced) on a CRLF STATE.md.
|
||||
const STATE_CRLF_SESSION_MISSING_STOPPED = [
|
||||
'---',
|
||||
'status: executing',
|
||||
'---',
|
||||
'',
|
||||
'## Session',
|
||||
'',
|
||||
'**Last session:** 2026-01-01T00:00:00.000Z',
|
||||
// Stopped at absent — forces the section-rewrite insert path
|
||||
'**Resume file:** None',
|
||||
'',
|
||||
].join('\r\n');
|
||||
|
||||
const d = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(path.join(d, '.planning', 'STATE.md'), STATE_CRLF_SESSION_MISSING_STOPPED);
|
||||
const result = runGsdTools(['state', 'record-session', '--stopped-at', '14.3'], d);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const after = fs.readFileSync(path.join(d, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(
|
||||
/\*\*Stopped at:\*\*\s*14\.3/.test(after),
|
||||
`Stopped at field must be on disk; STATE.md head:\n${after.slice(0, 500)}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(d);
|
||||
}
|
||||
});
|
||||
|
||||
test('record-session --resume-file on CRLF STATE.md with Session Continuity heading: field IS inserted (#2450)', () => {
|
||||
// Same bug class, different heading: `## Session Continuity` is the
|
||||
// bootstrap-template shape. The rewrite regex at the second bug site used
|
||||
// the same literal-\n pattern.
|
||||
const STATE_CRLF_CONTINUITY = [
|
||||
'---',
|
||||
'status: executing',
|
||||
'---',
|
||||
'',
|
||||
'## Session Continuity',
|
||||
'',
|
||||
'Next recommended action: resume phase 2.',
|
||||
'',
|
||||
].join('\r\n');
|
||||
|
||||
const d = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(path.join(d, '.planning', 'STATE.md'), STATE_CRLF_CONTINUITY);
|
||||
const result = runGsdTools(['state', 'record-session', '--resume-file', 'plan-9.md'], d);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const after = fs.readFileSync(path.join(d, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(
|
||||
/\*\*Resume file:\*\*\s*plan-9\.md/.test(after),
|
||||
`Resume file field must be inserted under Session Continuity; STATE.md head:\n${after.slice(0, 500)}`,
|
||||
);
|
||||
// Existing prose must be preserved (#1101 invariant)
|
||||
assert.ok(/Next recommended action: resume phase 2\./.test(after),
|
||||
'Session Continuity prose must be preserved');
|
||||
} finally {
|
||||
cleanup(d);
|
||||
}
|
||||
});
|
||||
|
||||
test('record-session --resume-file on mixed-ending STATE.md (LF frontmatter + CRLF body): field IS written (#2450)', () => {
|
||||
// CONTRIBUTING.md §"Parser and project-file inputs" lists "Mixed CRLF/LF
|
||||
// newlines" as a required adversarial fixture class. Realistic when a
|
||||
// Windows editor normalizes frontmatter bytes but preserves body text,
|
||||
// or when tooling concatenates LF + CRLF fragments. The bug class is
|
||||
// body-section-rewrite, so CRLF body + LF frontmatter is the worst case:
|
||||
// the frontmatter delimiter is LF (syncStateFrontmatter parses either)
|
||||
// but the `## Session` heading is CRLF, exercising the writer regex.
|
||||
const STATE_MIXED_LF_FM_CRLF_BODY = [
|
||||
'---',
|
||||
'status: executing',
|
||||
'---',
|
||||
'', // LF after frontmatter
|
||||
].join('\n') + [
|
||||
'## Session',
|
||||
'',
|
||||
'**Last session:** 2026-01-01T00:00:00.000Z',
|
||||
'**Stopped at:** None',
|
||||
// Resume file absent
|
||||
'',
|
||||
].join('\r\n');
|
||||
|
||||
const d = createTempProject();
|
||||
try {
|
||||
fs.writeFileSync(path.join(d, '.planning', 'STATE.md'), STATE_MIXED_LF_FM_CRLF_BODY);
|
||||
const result = runGsdTools(['state', 'record-session', '--resume-file', 'plan-mix.md'], d);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const after = fs.readFileSync(path.join(d, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(
|
||||
/\*\*Resume file:\*\*\s*plan-mix\.md/.test(after),
|
||||
`Resume file field must be on disk despite mixed line endings; STATE.md head:\n${after.slice(0, 500)}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(d);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('T6 section-splice characterization — add-decision', () => {
|
||||
|
||||
Reference in New Issue
Block a user