feat(sdk): golden parity harness and query handler CJS alignment (#2302 Track A) (#2341)

* feat(sdk): golden parity harness and query handler CJS alignment (#2302 Track A)

Golden/read-only parity tests and registry alignment, query handler fixes
(check-completion, state-mutation, commit, validate, summary, etc.), and
WAITING.json dual-write for .gsd/.planning readers.

Refs gsd-build/get-shit-done#2341

* fix(sdk): getMilestoneInfo matches GSD ROADMAP (🟡, last bold, STATE fallback)

- Recognize in-flight 🟡 milestone bullets like 🚧.
- Derive from last **vX.Y Title** before ## Phases when emoji absent.
- Fall back to STATE.md milestone when ROADMAP is missing; use last bare vX.Y
  in cleaned text instead of first (avoids v1.0 from shipped list).
- Fixes init.execute-phase milestone_version and buildStateFrontmatter after
  state.begin-phase (syncStateFrontmatter).

* feat(sdk): phase list, plan task structure, requirements extract handlers

- Register phase.list-plans, phase.list-artifacts, plan.task-structure,
  requirements.extract-from-plans (SDK-only; golden-policy exceptions).
- Add unit tests; document in QUERY-HANDLERS.md.
- writeProfile: honor --output, render dimensions, return profile_path and dimensions_scored.

* feat(sdk): centralize getGsdAgentsDir in query helpers

Extract agent directory resolution to helpers (GSD_AGENTS_DIR, primary
~/.claude/agents, legacy path). Use from init and docs-init init bundles.

docs(15): add 15-CONTEXT for autonomous phase-15 run.

* feat(sdk): query CLI CJS fallback and session correlation

- createRegistry(eventStream, sessionId) threads correlation into mutation events
- gsd-sdk query falls back to gsd-tools.cjs when no native handler matches
  (disable with GSD_QUERY_FALLBACK=off); stderr bridge warnings
- Export createRegistry from @gsd-build/sdk; add sdk/README.md
- Update QUERY-HANDLERS.md and registry module docs for fallback + sessionId
- Agents: prefer node dist/cli.js query over cat/grep for STATE and plans

* fix(sdk): init phase_found parity, docs-init agents path, state field extract

- Normalize findPhase not-found to null before roadmap fallback (matches findPhaseInternal)

- docs-init: use detectRuntime + resolveAgentsDir for checkAgentsInstalled

- state.cjs stateExtractField: horizontal whitespace only after colon (YAML progress guard)

- Tests: commit_docs default true; config-get golden uses temp config; golden integration green

Refs: #2302

* refactor(sdk): share SessionJsonlRecord in profile-extract-messages

CodeRabbit nit: dedupe JSONL record shape for isGenuineUserMessage and streamExtractMessages.

* fix(sdk): address CodeRabbit major threads (paths, gates, audit, verify)

- Resolve @file: and CLI JSON indirection relative to projectDir; guard empty normalized query command

- plan.task-structure + intel extract/patch-meta: resolvePathUnderProject containment

- check.config-gates: safe string booleans; plan_checker alias precedence over plan_check default

- state.validate/sync: phaseTokenMatches + comparePhaseNum ordering

- verify.schema-drift: token match phase dirs; files_modified from parsed frontmatter

- audit-open: has_scan_errors, unreadable rows, human report when scans fail

- requirements PLANNED key PLAN for root PLAN.md; gsd-tools timeout note

- ingest-docs: repo-root path containment; classifier output slug-hash

Golden parity test strips has_scan_errors until CJS adds field.

* fix: Resolve CodeRabbit security and quality findings
- Secure intel.ts and cli.ts against path traversal
- Catch and validate git add status in commit.ts
- Expand roadmap milestone marker extraction
- Fix parsing array-of-objects in frontmatter YAML
- Fix unhandled config evaluations
- Improve coverage test parity mapping

* test: raise planner character extraction limit to 48K

* fix(sdk): resolve TS build error in docs-init passing config
This commit is contained in:
Rezolv
2026-04-20 18:09:02 -04:00
committed by GitHub
parent c8807e38d7
commit c5b1445529
109 changed files with 11588 additions and 1030 deletions

View File

@@ -1,5 +1,5 @@
/**
* Workstream query handlers — list, create, set, status, complete, progress.
* Workstream query handlers — list, get, create, set, status, complete, progress.
*
* Ported from get-shit-done/bin/lib/workstream.cjs.
* Manages .planning/workstreams/ directory for multi-workstream projects.
@@ -20,10 +20,10 @@ import {
existsSync, readdirSync, readFileSync, writeFileSync,
mkdirSync, renameSync, rmdirSync, unlinkSync,
} from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { toPosixPath } from './helpers.js';
import { toPosixPath, stateExtractField } from './helpers.js';
import { GSDError, ErrorClassification } from '../errors.js';
import type { QueryHandler } from './utils.js';
// ─── Internal helpers ─────────────────────────────────────────────────────
@@ -34,6 +34,30 @@ const planningRoot = (projectDir: string) =>
const workstreamsDir = (projectDir: string) =>
join(planningRoot(projectDir), 'workstreams');
function wsPlanningPaths(projectDir: string, name: string) {
const base = join(planningRoot(projectDir), 'workstreams', name);
return {
planning: base,
state: join(base, 'STATE.md'),
roadmap: join(base, 'ROADMAP.md'),
phases: join(base, 'phases'),
requirements: join(base, 'REQUIREMENTS.md'),
};
}
function readSubdirectories(dir: string): string[] {
if (!existsSync(dir)) return [];
return readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name);
}
function filterPlanFiles(files: string[]): string[] {
return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
}
function filterSummaryFiles(files: string[]): string[] {
return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
}
function getActiveWorkstream(projectDir: string): string | null {
const filePath = join(planningRoot(projectDir), 'active-workstream');
try {
@@ -67,6 +91,22 @@ function setActiveWorkstream(projectDir: string, name: string | null): void {
// ─── Handlers ─────────────────────────────────────────────────────────────
/**
* Current active workstream and mode (flat vs workstream).
*
* Port of `cmdWorkstreamGet` from `workstream.cjs` lines 367–371.
*/
export const workstreamGet: QueryHandler = async (_args, projectDir) => {
const active = getActiveWorkstream(projectDir);
const wsRoot = workstreamsDir(projectDir);
return {
data: {
active,
mode: existsSync(wsRoot) ? 'workstream' : 'flat',
},
};
};
export const workstreamList: QueryHandler = async (_args, projectDir) => {
const dir = workstreamsDir(projectDir);
if (!existsSync(dir)) return { data: { mode: 'flat', workstreams: [], message: 'No workstreams — operating in flat mode' } };
@@ -170,16 +210,75 @@ export const workstreamSet: QueryHandler = async (args, projectDir) => {
return { data: { active: null, error: 'not_found', workstream: name } };
}
const previous = getActiveWorkstream(projectDir);
setActiveWorkstream(projectDir, name);
return { data: { active: name, previous: previous || null, set: true } };
return { data: { active: name, set: true } };
};
export const workstreamStatus: QueryHandler = async (args, projectDir) => {
const name = args[0];
if (!name) return { data: { found: false, reason: 'name required' } };
if (!name) {
throw new GSDError('workstream name required. Usage: workstream status <name>', ErrorClassification.Validation);
}
if (/[/\\]/.test(name) || name === '.' || name === '..') {
throw new GSDError('Invalid workstream name', ErrorClassification.Validation);
}
const wsDir = join(workstreamsDir(projectDir), name);
return { data: { name, found: existsSync(wsDir), path: toPosixPath(relative(projectDir, wsDir)) } };
if (!existsSync(wsDir)) {
return { data: { found: false, workstream: name } };
}
const p = wsPlanningPaths(projectDir, name);
const relPath = toPosixPath(relative(projectDir, wsDir));
const files = {
roadmap: existsSync(p.roadmap),
state: existsSync(p.state),
requirements: existsSync(p.requirements),
};
const phases: Array<{ directory: string; status: string; plan_count: number; summary_count: number }> = [];
for (const dir of readSubdirectories(p.phases).sort()) {
try {
const phaseFiles = readdirSync(join(p.phases, dir));
const plans = filterPlanFiles(phaseFiles);
const summaries = filterSummaryFiles(phaseFiles);
phases.push({
directory: dir,
status:
summaries.length >= plans.length && plans.length > 0
? 'complete'
: plans.length > 0
? 'in_progress'
: 'pending',
plan_count: plans.length,
summary_count: summaries.length,
});
} catch { /* skip */ }
}
let stateInfo: Record<string, string | null> = {};
try {
const stateContent = readFileSync(p.state, 'utf-8');
stateInfo = {
status: stateExtractField(stateContent, 'Status') || 'unknown',
current_phase: stateExtractField(stateContent, 'Current Phase'),
last_activity: stateExtractField(stateContent, 'Last Activity'),
};
} catch { /* skip */ }
return {
data: {
found: true,
workstream: name,
path: relPath,
files,
phases,
phase_count: phases.length,
completed_phases: phases.filter(ph => ph.status === 'complete').length,
...stateInfo,
},
};
};
export const workstreamComplete: QueryHandler = async (args, projectDir) => {
@@ -246,7 +345,90 @@ export const workstreamComplete: QueryHandler = async (args, projectDir) => {
};
};
export const workstreamProgress: QueryHandler = async (args, projectDir) => {
const { progressBar } = await import('./progress.js');
return progressBar(args, projectDir);
/**
* Port of `cmdWorkstreamProgress` from `workstream.cjs` — aggregate status for each workstream.
* (Not the same as roadmap `progress` / `progressBar`.)
*/
export const workstreamProgress: QueryHandler = async (_args, projectDir) => {
const wsRoot = workstreamsDir(projectDir);
if (!existsSync(wsRoot)) {
return {
data: {
mode: 'flat',
workstreams: [],
message: 'No workstreams — operating in flat mode',
},
};
}
const active = getActiveWorkstream(projectDir);
const entries = readdirSync(wsRoot, { withFileTypes: true });
const workstreams: Array<{
name: string;
active: boolean;
status: string;
current_phase: string | null;
phases: string;
plans: string;
progress_percent: number;
}> = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const wsDir = join(wsRoot, entry.name);
const phasesDir = join(wsDir, 'phases');
const phaseDirsProgress = readSubdirectories(phasesDir);
const phaseCount = phaseDirsProgress.length;
let completedCount = 0;
let totalPlans = 0;
let completedPlans = 0;
for (const d of phaseDirsProgress) {
try {
const phaseFiles = readdirSync(join(phasesDir, d));
const plans = filterPlanFiles(phaseFiles);
const summaries = filterSummaryFiles(phaseFiles);
totalPlans += plans.length;
completedPlans += Math.min(summaries.length, plans.length);
if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
} catch { /* skip */ }
}
let roadmapPhaseCount = phaseCount;
try {
const roadmapContent = readFileSync(join(wsDir, 'ROADMAP.md'), 'utf-8');
const phaseMatches = roadmapContent.match(/^###?\s+Phase\s+\d/gm);
if (phaseMatches) roadmapPhaseCount = phaseMatches.length;
} catch { /* no roadmap */ }
let status = 'unknown';
let currentPhase: string | null = null;
try {
const stateContent = readFileSync(join(wsDir, 'STATE.md'), 'utf-8');
status = stateExtractField(stateContent, 'Status') || 'unknown';
currentPhase = stateExtractField(stateContent, 'Current Phase');
} catch { /* skip */ }
workstreams.push({
name: entry.name,
active: entry.name === active,
status,
current_phase: currentPhase,
phases: `${completedCount}/${roadmapPhaseCount}`,
plans: `${completedPlans}/${totalPlans}`,
progress_percent:
roadmapPhaseCount > 0 ? Math.round((completedCount / roadmapPhaseCount) * 100) : 0,
});
}
return {
data: {
mode: 'workstream',
active,
workstreams,
count: workstreams.length,
},
};
};