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

@@ -28,16 +28,17 @@ import { extractFrontmatter, stripFrontmatter } from './frontmatter.js';
import { reconstructFrontmatter, spliceFrontmatter } from './frontmatter-mutation.js';
import {
comparePhaseNum,
escapeRegex,
normalizePhaseName,
phaseTokenMatches,
planningPaths,
normalizeMd,
stateExtractField,
} from './helpers.js';
import { buildStateFrontmatter, getMilestonePhaseFilter } from './state.js';
import { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } from './state-document.js';
import type { QueryHandler } from './utils.js';
const PROGRESS_FRONTMATTER_FIELDS = new Set(['Progress', 'Total Plans in Phase', 'Total Phases']);
// ─── Process exit lock cleanup (D2 — match CJS state.cjs:16-23) ─────────
/**
@@ -52,48 +53,7 @@ process.on('exit', () => {
}
});
// ─── stateReplaceField ────────────────────────────────────────────────────
/**
* Replace a field value in STATE.md content.
*
* Uses separate regex instances (no g flag) to avoid lastIndex persistence.
* Supports both **bold:** and plain: formats.
*
* @param content - STATE.md content
* @param fieldName - Field name to replace
* @param newValue - New value to set
* @returns Updated content, or null if field not found
*/
export function stateReplaceField(content: string, fieldName: string, newValue: string): string | null {
const escaped = escapeRegex(fieldName);
// Try **Field:** bold format first
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
if (boldPattern.test(content)) {
return content.replace(new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i'), (_match, prefix: string) => `${prefix}${newValue}`);
}
// Try plain Field: format
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
if (plainPattern.test(content)) {
return content.replace(new RegExp(`(^${escaped}:\\s*)(.*)`, 'im'), (_match, prefix: string) => `${prefix}${newValue}`);
}
return null;
}
/**
* Replace a field with fallback field name support.
*
* Tries primary first, then fallback. Returns content unchanged if neither matches.
*/
function stateReplaceFieldWithFallback(content: string, primary: string, fallback: string | null, value: string): string {
let result = stateReplaceField(content, primary, value);
if (result) return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result) return result;
}
return content;
}
export { stateReplaceField };
/**
* Update fields within the ## Current Position section.
@@ -234,10 +194,15 @@ export async function releaseStateLock(lockPath: string): Promise<void> {
* Strips existing frontmatter, rebuilds from body + disk, and splices back.
* Preserves existing status when body-derived status is 'unknown'.
*/
async function syncStateFrontmatter(content: string, projectDir: string): Promise<string> {
async function syncStateFrontmatter(
content: string,
projectDir: string,
workstream?: string,
options: { preserveExistingProgress?: boolean } = {},
): Promise<string> {
const existingFm = extractFrontmatter(content);
const body = stripFrontmatter(content);
const derivedFm = await buildStateFrontmatter(body, projectDir);
const derivedFm = await buildStateFrontmatter(body, projectDir, workstream, options);
// Preserve existing status when body-derived is 'unknown'
if (derivedFm.status === 'unknown' && existingFm.status && existingFm.status !== 'unknown') {
@@ -261,8 +226,10 @@ async function readModifyWriteStateMd(
projectDir: string,
modifier: (content: string) => string | Promise<string>,
workstream?: string,
options: { resync?: boolean; preserveExistingProgress?: boolean } = {},
): Promise<string> {
const statePath = planningPaths(projectDir, workstream).state;
const resync = options.resync !== false;
const lockPath = await acquireStateLock(statePath);
try {
let content: string;
@@ -274,9 +241,18 @@ async function readModifyWriteStateMd(
// Strip frontmatter before passing to modifier so that regex replacements
// operate on body fields only (not on YAML frontmatter keys like 'status:').
// syncStateFrontmatter rebuilds frontmatter from the modified body + disk.
const preFm = extractFrontmatter(content);
const body = stripFrontmatter(content);
const modified = await modifier(body);
const synced = await syncStateFrontmatter(modified, projectDir);
let synced = await syncStateFrontmatter(modified, projectDir, workstream, {
preserveExistingProgress: options.preserveExistingProgress,
});
if (!resync && preFm && preFm.progress) {
const postFm = extractFrontmatter(synced);
postFm.progress = preFm.progress;
const yamlStr = reconstructFrontmatter(postFm);
synced = `---\n${yamlStr}\n---\n\n${stripFrontmatter(synced)}`;
}
const normalized = normalizeMd(synced);
await writeFile(statePath, normalized, 'utf-8');
return normalized;
@@ -305,7 +281,7 @@ export async function readModifyWriteStateMdFull(
/* missing */
}
const modified = await modifier(content);
const synced = await syncStateFrontmatter(modified, projectDir);
const synced = await syncStateFrontmatter(modified, projectDir, workstream);
await writeFile(statePath, normalizeMd(synced), 'utf-8');
} finally {
await releaseStateLock(lockPath);
@@ -332,6 +308,7 @@ export const stateUpdate: QueryHandler = async (args, projectDir, workstream) =>
}
let updated = false;
const shouldResync = PROGRESS_FRONTMATTER_FIELDS.has(field);
await readModifyWriteStateMd(projectDir, (content) => {
const result = stateReplaceField(content, field, value);
if (result) {
@@ -339,7 +316,10 @@ export const stateUpdate: QueryHandler = async (args, projectDir, workstream) =>
return result;
}
return content;
}, workstream);
}, workstream, {
resync: shouldResync,
preserveExistingProgress: !shouldResync,
});
return { data: { updated } };
};
@@ -377,6 +357,7 @@ export const statePatch: QueryHandler = async (args, projectDir, workstream) =>
const updated: string[] = [];
const failed: string[] = [];
const shouldResync = Object.keys(patches).some(field => PROGRESS_FRONTMATTER_FIELDS.has(field));
await readModifyWriteStateMd(projectDir, (content) => {
for (const [field, value] of Object.entries(patches)) {
const result = stateReplaceField(content, field, String(value));
@@ -388,7 +369,10 @@ export const statePatch: QueryHandler = async (args, projectDir, workstream) =>
}
}
return content;
}, workstream);
}, workstream, {
resync: shouldResync,
preserveExistingProgress: !shouldResync,
});
return { data: { updated, failed } };
};
@@ -1707,4 +1691,4 @@ export const statePrune: QueryHandler = async (args, projectDir, workstream) =>
archive_file: totalPruned > 0 ? 'STATE-ARCHIVE.md' : null,
},
};
};
};