diff --git a/sdk/src/query/phase-list-queries.ts b/sdk/src/query/phase-list-queries.ts
index 17335cfc0..65aae7ebe 100644
--- a/sdk/src/query/phase-list-queries.ts
+++ b/sdk/src/query/phase-list-queries.ts
@@ -17,7 +17,7 @@ import {
import type { QueryHandler } from './utils.js';
/** Resolve `.planning/phases/
` for a phase token, or null. */
-async function resolvePhaseDir(phase: string, projectDir: string, workstream?: string): Promise {
+export async function resolvePhaseDir(phase: string, projectDir: string, workstream?: string): Promise {
const phasesDir = planningPaths(projectDir, workstream).phases;
const normalized = normalizePhaseName(phase);
try {
diff --git a/sdk/src/query/phase-uat-passed.test.ts b/sdk/src/query/phase-uat-passed.test.ts
new file mode 100644
index 000000000..46ad6993f
--- /dev/null
+++ b/sdk/src/query/phase-uat-passed.test.ts
@@ -0,0 +1,45 @@
+/**
+ * Unit tests for isPhaseUatPassed — walking skeleton (cycle 1 of ~15).
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+import { isPhaseUatPassed } from './phase-uat-passed.js';
+
+const UAT_PASS_CONTENT = `---
+status: complete
+phase: 5
+source: roadmap
+started: 2026-05-18T00:00:00Z
+updated: 2026-05-18T00:00:00Z
+---
+
+### 1. First item
+expected: thing should happen
+result: pass
+`;
+
+let tmpDir: string;
+
+beforeEach(async () => {
+ tmpDir = await mkdtemp(join(tmpdir(), 'gsd-uat-passed-'));
+ const phaseDir = join(tmpDir, '.planning', 'phases', '05-walking-skeleton');
+ await mkdir(phaseDir, { recursive: true });
+ await writeFile(join(phaseDir, '05-HUMAN-UAT.md'), UAT_PASS_CONTENT);
+});
+
+afterEach(async () => {
+ await rm(tmpDir, { recursive: true, force: true });
+});
+
+describe('isPhaseUatPassed', () => {
+ it('returns passed=true when a single UAT file contains one pass result', async () => {
+ const result = await isPhaseUatPassed(tmpDir, '5');
+ expect(result.passed).toBe(true);
+ expect(result.items.length).toBe(1);
+ expect(result.items[0].result).toBe('pass');
+ expect(result.reasons.length).toBe(0);
+ });
+});
diff --git a/sdk/src/query/phase-uat-passed.ts b/sdk/src/query/phase-uat-passed.ts
new file mode 100644
index 000000000..8f72eb0bf
--- /dev/null
+++ b/sdk/src/query/phase-uat-passed.ts
@@ -0,0 +1,60 @@
+/**
+ * isPhaseUatPassed — SDK predicate answering "is phase N's UAT contract satisfied?"
+ *
+ * Cycle 1 of ~15 (walking skeleton): happy path only. No injection stripping,
+ * no orphan detection, no human_verification frontmatter merge, no REASON_CODEs.
+ */
+
+import { readFile, readdir } from 'node:fs/promises';
+import { join } from 'node:path';
+import { resolvePhaseDir } from './phase-list-queries.js';
+
+/** Regex to parse all UAT items regardless of result value. */
+const UAT_ITEM_PATTERN =
+ /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)/g;
+
+function parseAllUatItems(content: string): Record[] {
+ const items: Record[] = [];
+ UAT_ITEM_PATTERN.lastIndex = 0;
+ let m: RegExpMatchArray | null;
+ while ((m = UAT_ITEM_PATTERN.exec(content)) !== null) {
+ const [, num, name, expected, result] = m;
+ items.push({
+ test: parseInt(num, 10),
+ name: name.trim(),
+ expected: expected.trim(),
+ result,
+ });
+ }
+ UAT_ITEM_PATTERN.lastIndex = 0;
+ return items;
+}
+
+export async function isPhaseUatPassed(
+ projectDir: string,
+ phase: string,
+ workstream?: string,
+): Promise<{
+ passed: boolean;
+ reasons: unknown[];
+ reasonsHuman: string[];
+ items: Record[];
+}> {
+ const dir = await resolvePhaseDir(phase, projectDir, workstream);
+ if (!dir) {
+ return { passed: false, reasons: [], reasonsHuman: [], items: [] };
+ }
+
+ const files = await readdir(dir);
+ const uatFiles = files.filter((f) => f.endsWith('-HUMAN-UAT.md'));
+
+ const items: Record[] = [];
+ for (const file of uatFiles) {
+ const content = await readFile(join(dir, file), 'utf-8');
+ items.push(...parseAllUatItems(content));
+ }
+
+ const passed = items.length > 0 && items.every((i) => i.result === 'pass');
+
+ return { passed, reasons: [], reasonsHuman: [], items };
+}