feat(sdk): Phase 1 typed query foundation (gsd-sdk query) (#2118)

* feat(sdk): add typed query foundation and gsd-sdk query (Phase 1)

Add sdk/src/query registry and handlers with tests, GSDQueryError, CLI query wiring, and supporting type/tool-scoping hooks. Update CHANGELOG. Vitest 4 constructor mock fixes in milestone-runner tests.

Made-with: Cursor

* chore: gitignore .cursor for local-only Cursor assets

Made-with: Cursor

* fix(sdk): harden query layer for PR review (paths, locks, CLI, ReDoS)

- resolvePathUnderProject: realpath + relative containment for frontmatter and key_links

- commitToSubrepo: path checks + sanitizeCommitMessage

- statePlannedPhase: readModifyWriteStateMd (lock); MUTATION_COMMANDS + events

- key_links: regexForKeyLinkPattern length/ReDoS guard; phase dirs: reject .. and separators

- gsd-sdk: strip --pick before parseArgs; strict parser; QueryRegistry.commands()

- progress: static GSDError import; tests updated

Made-with: Cursor

* feat(sdk): query follow-up — tests, QUERY-HANDLERS, registry, locks, intel depth

Made-with: Cursor

* docs(sdk): use ASCII punctuation in QUERY-HANDLERS.md

Made-with: Cursor
This commit is contained in:
Rezolv
2026-04-12 18:15:04 -04:00
committed by GitHub
parent 66a5f939b0
commit 6f79b1dd5e
36 changed files with 848 additions and 142 deletions

View File

@@ -112,11 +112,32 @@ function updateCurrentPositionFields(content: string, fields: Record<string, str
// ─── Lockfile helpers ─────────────────────────────────────────────────────
/**
* If the lock file contains a PID, return whether that process is gone (stolen
* locks after SIGKILL/crash). Null if the file could not be read.
*/
async function isLockProcessDead(lockPath: string): Promise<boolean | null> {
try {
const raw = await readFile(lockPath, 'utf-8');
const pid = parseInt(raw.trim(), 10);
if (!Number.isFinite(pid) || pid <= 0) return true;
try {
process.kill(pid, 0);
return false;
} catch {
return true;
}
} catch {
return null;
}
}
/**
* Acquire a lockfile for STATE.md operations.
*
* Uses O_CREAT|O_EXCL for atomic creation. Retries up to 10 times with
* 200ms + jitter delay. Cleans stale locks older than 10 seconds.
* 200ms + jitter delay. Cleans stale locks when the holder PID is dead, or when
* the lock file is older than 10 seconds (existing heuristic).
*
* @param statePath - Path to STATE.md
* @returns Path to the lockfile
@@ -136,6 +157,11 @@ export async function acquireStateLock(statePath: string): Promise<string> {
} catch (err: unknown) {
if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'EEXIST') {
try {
const dead = await isLockProcessDead(lockPath);
if (dead === true) {
await unlink(lockPath);
continue;
}
const s = await stat(lockPath);
if (Date.now() - s.mtimeMs > 10000) {
await unlink(lockPath);
@@ -714,22 +740,20 @@ export const statePlannedPhase: QueryHandler = async (args, projectDir) => {
const phaseArg = args.find((a, i) => args[i - 1] === '--phase') || args[0];
const nameArg = args.find((a, i) => args[i - 1] === '--name') || '';
const plansArg = args.find((a, i) => args[i - 1] === '--plans') || '0';
const paths = planningPaths(projectDir);
if (!phaseArg) {
return { data: { updated: false, reason: '--phase argument required' } };
}
try {
let content = await readFile(paths.state, 'utf-8');
const timestamp = new Date().toISOString();
const record = `\n**Planned Phase:** ${phaseArg} (${nameArg}) — ${plansArg} plans — ${timestamp}\n`;
if (/\*\*Planned Phase:\*\*/.test(content)) {
content = content.replace(/\*\*Planned Phase:\*\*[^\n]*\n/, record);
} else {
content += record;
}
await writeFile(paths.state, content, 'utf-8');
await readModifyWriteStateMd(projectDir, (body) => {
if (/\*\*Planned Phase:\*\*/.test(body)) {
return body.replace(/\*\*Planned Phase:\*\*[^\n]*\n/, record);
}
return body + record;
});
return { data: { updated: true, phase: phaseArg, name: nameArg, plans: plansArg } };
} catch {
return { data: { updated: false, reason: 'STATE.md not found or unreadable' } };