Refactor SDK-first architecture seams (#3316)

* refactor: tighten sdk-first architecture seams

Refs #3312

* refactor: finish state document seam cleanup

Refs #3312

* test: harden minimal install cleanup assertion

* ci: support sdk-scoped package lock

* fix(3316): restore root package-lock.json and align changeset pr ref

Reverts dec57a83 ("ci: support sdk-scoped package lock") and restores
the root package-lock.json that c249d34d deleted. The deletion was the
wrong direction:

- The root package.json declares its own runtime and dev deps
  (@anthropic-ai/claude-agent-sdk, ws, c8). Without a root lockfile,
  `npm install --no-package-lock` resolves whatever satisfies semver at
  install time — CI today and CI in six months can install different
  transitive trees, defeating reproducibility.
- The lockfile has been part of every release on this repo (long
  history on main); removing it loses the npm audit / Dependabot
  target without compensating benefit.
- The CI workaround pattern (cache-dependency-path: sdk/package-lock.json
  + `npm install --no-package-lock`) papered over the symptom rather
  than fix the cause.

Also fix the changeset pr: from 3312 (issue) to 3316 (PR). CONTEXT.md
flags this exact failure mode as a recurring CodeRabbit finding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address coderabbit review findings

* fix: close remaining coderabbit threads

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-09 14:21:42 -04:00
committed by GitHub
parent 2e87c60afc
commit 26dcdb1ad0
34 changed files with 1693 additions and 738 deletions

View File

@@ -244,6 +244,131 @@ describe('stateUpdate', () => {
expect(data.updated).toBe(false);
});
it('preserves curated progress frontmatter during body-only updates', async () => {
const stateContent = `---
gsd_state_version: 1.0
milestone: v3.0
milestone_name: SDK-First Migration
status: executing
progress:
total_phases: 12
completed_phases: 6
total_plans: 22
completed_plans: 22
percent: 50
---
# Project State
## Current Position
Status: Executing
Last Activity: 2026-01-01
Progress: [█████░░░░░] 50%
`;
await setupTestProject(tmpDir, stateContent);
const { stateUpdate } = await import('./state-mutation.js');
const { stateJson } = await import('./state.js');
const result = await stateUpdate(['Last Activity', '2026-05-07'], tmpDir);
expect((result.data as Record<string, unknown>).updated).toBe(true);
const loaded = await stateJson([], tmpDir);
const progress = (loaded.data as Record<string, unknown>).progress as Record<string, unknown>;
expect(Number(progress.total_phases)).toBe(12);
expect(Number(progress.completed_phases)).toBe(6);
expect(Number(progress.total_plans)).toBe(22);
expect(Number(progress.completed_plans)).toBe(22);
expect(Number(progress.percent)).toBe(50);
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
expect(after).toContain('Last Activity: 2026-05-07');
});
it('resyncs progress frontmatter when updating the Progress body field', async () => {
const stateContent = `---
gsd_state_version: 1.0
milestone: v3.0
milestone_name: SDK-First Migration
status: executing
progress:
total_phases: 12
completed_phases: 6
total_plans: 22
completed_plans: 22
percent: 50
---
# Project State
## Current Position
Status: Executing
Progress: [█████░░░░░] 50%
`;
await setupTestProject(tmpDir, stateContent);
await rm(join(tmpDir, '.planning', 'ROADMAP.md'), { force: true });
const { stateUpdate } = await import('./state-mutation.js');
await stateUpdate(['Progress', '[████████░░] 80%'], tmpDir);
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
const { extractFrontmatter } = await import('./frontmatter.js');
const fm = extractFrontmatter(after);
const progress = fm.progress as Record<string, unknown>;
expect(Number(progress.percent)).toBe(80);
});
it('syncs full-file workstream STATE.md frontmatter from the selected workstream', async () => {
const planningDir = join(tmpDir, '.planning');
const wsDir = join(planningDir, 'workstreams', 'feature');
await mkdir(join(wsDir, 'phases'), { recursive: true });
await writeFile(
join(planningDir, 'STATE.md'),
[
'---',
'milestone: v0.1',
'milestone_name: Root Milestone',
'---',
'',
'# Root State',
'',
'Status: Root',
'',
].join('\n'),
'utf-8',
);
await writeFile(join(planningDir, 'ROADMAP.md'), '# Roadmap\n\n## v0.1 Root Milestone\n', 'utf-8');
await writeFile(
join(wsDir, 'STATE.md'),
[
'---',
'milestone: v2.0',
'milestone_name: Feature Milestone',
'---',
'',
'# Workstream State',
'',
'Status: Executing',
'',
].join('\n'),
'utf-8',
);
await writeFile(join(wsDir, 'ROADMAP.md'), '# Roadmap\n\n## v2.0 Feature Milestone\n', 'utf-8');
const { readModifyWriteStateMdFull } = await import('./state-mutation.js');
await readModifyWriteStateMdFull(tmpDir, content => content.replace('Status: Executing', 'Status: Complete'), 'feature');
const after = await readFile(join(wsDir, 'STATE.md'), 'utf-8');
const { extractFrontmatter } = await import('./frontmatter.js');
const fm = extractFrontmatter(after);
expect(fm.milestone).toBe('v2.0');
expect(fm.milestone_name).toBe('Feature Milestone');
expect(fm.status).toBe('completed');
});
it('throws on missing args', async () => {
const { stateUpdate } = await import('./state-mutation.js');
@@ -277,6 +402,45 @@ describe('statePatch', () => {
const content = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
expect(content).toContain('done');
});
it('preserves curated progress frontmatter when patching body-only fields', async () => {
const stateContent = `---
gsd_state_version: 1.0
milestone: v3.0
milestone_name: SDK-First Migration
status: executing
progress:
total_phases: 12
completed_phases: 6
total_plans: 22
completed_plans: 22
percent: 50
---
# Project State
## Current Position
Status: Executing
Last Activity: 2026-01-01
Progress: [█████░░░░░] 50%
`;
await setupTestProject(tmpDir, stateContent);
const { statePatch } = await import('./state-mutation.js');
await statePatch([JSON.stringify({ 'Last Activity': '2026-05-07' })], tmpDir);
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
const { extractFrontmatter } = await import('./frontmatter.js');
const fm = extractFrontmatter(after);
const progress = fm.progress as Record<string, unknown>;
expect(Number(progress.total_phases)).toBe(12);
expect(Number(progress.completed_phases)).toBe(6);
expect(Number(progress.total_plans)).toBe(22);
expect(Number(progress.completed_plans)).toBe(22);
expect(Number(progress.percent)).toBe(50);
expect(after).toContain('Last Activity: 2026-05-07');
});
});
// ─── stateBeginPhase ────────────────────────────────────────────────────────