Compare commits

..

8 Commits

Author SHA1 Message Date
xiaoju 7ca3ad5d65 debug: dump raw NDJSON for issue #439 investigation
Saves Claude Code stdout to /tmp/uwf-ndjson-dump/ before parsing.
Temporary — remove after root cause confirmed.

Refs #439
2026-05-23 12:24:05 +00:00
xiaomo 52879c0028 Merge pull request 'feat(cli-workflow): implement multi-strategy workflow resolution' (#438) from fix/428-multi-strategy-workflow-resolution into main 2026-05-23 11:12:56 +00:00
xiaoju 8720eb19af feat(cli-workflow): implement multi-strategy workflow resolution for issue #428
- Add 4-strategy resolution priority: CAS hash → file path → local discovery → global registry
- Add helper functions: isFilePath, workflowFileExists, findWorkflowInDir, findWorkflowInParents
- Refactor resolveWorkflowCasRef to support direct hash, explicit paths, and parent traversal
- Add comprehensive test suite with 24 tests covering all strategies and edge cases
- Support .workflow/ and .workflows/ directories with .yaml/.yml extensions
- All 60 tests pass across 5 test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:11:37 +00:00
xiaomo 9e4527bb89 Merge pull request 'fix(cli): disable YAML anchor/alias in output' (#437) from fix/yaml-no-alias into main 2026-05-23 11:09:11 +00:00
xingyue 5209cfa7ac fix(cli): disable YAML anchor/alias + fix biome errors in setup.ts
- Disable aliasDuplicateObjects in YAML stringify to prevent &a1/*a1
  anchors when multiple steps have identical output
- Fix unused discoverAgents function (prefixed with _) and format issue
  in setup.ts
2026-05-23 19:07:36 +08:00
xiaoju 155b879d29 chore(workflow): developer must rebase main when bounced back
Prevents duplicate lint fixes when main already has the fixes.

小橘 🍊(NEKO Team)
2026-05-23 10:57:44 +00:00
xiaomo c1f04929f4 Merge pull request 'feat(builtin-agent): persist ReAct loop turns as session JSONL' (#434) from feat/turn-jsonl-session into main 2026-05-23 10:48:49 +00:00
xiaoju 211f38bc8d fix(claude-code): include edge prompt in agent prompt as Current Instruction
buildClaudeCodePrompt was dropping ctx.edgePrompt entirely — the graph
transition instruction (e.g. 'Implement the plan') never reached the agent.
Now appended as '## Current Instruction' at the end of the prompt.
2026-05-23 09:46:17 +00:00
6 changed files with 564 additions and 14 deletions
+2 -1
View File
@@ -41,7 +41,8 @@ roles:
Before starting any work, ensure a clean worktree:
1. `git checkout main && git pull` to get the latest code
2. `git checkout -b fix/<issue-number>-<short-description>` to create a fresh branch
- If bounced back from reviewer or tester, reuse the existing branch instead
- If bounced back from reviewer or tester, reuse the existing branch and rebase onto latest main:
`git checkout main && git pull && git checkout <branch> && git rebase main`
Then implement TDD:
3. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's frontmatter.plan)
@@ -0,0 +1,367 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { CasRef, WorkflowPayload } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { stringify } from "yaml";
import { cmdThreadStart } from "../commands/thread.js";
import { registerUwfSchemas } from "../schemas.js";
import type { UwfStore } from "../store.js";
import { loadWorkflowRegistry, saveWorkflowRegistry } from "../store.js";
// ── helpers ───────────────────────────────────────────────────────────────────
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
return { storageRoot, store, schemas };
}
async function storeWorkflow(uwf: UwfStore, name: string): Promise<CasRef> {
const payload: WorkflowPayload = {
name,
description: "Test workflow",
roles: {},
conditions: {},
graph: {},
};
return await uwf.store.put(uwf.schemas.workflow, payload);
}
async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
const payload: WorkflowPayload = {
name,
description: version !== null ? `Test workflow (${version})` : "Test workflow",
roles: {},
conditions: {},
graph: {},
};
const yaml = stringify(payload);
return yaml;
}
// ── fixture ───────────────────────────────────────────────────────────────────
let tmpDir: string;
let storageRoot: string;
let projectRoot: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-wf-resolve-test-"));
storageRoot = join(tmpDir, "storage");
projectRoot = join(tmpDir, "project");
await mkdir(storageRoot, { recursive: true });
await mkdir(projectRoot, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
// ── Strategy 1: CAS Hash Resolution ───────────────────────────────────────────
describe("Strategy 1: CAS Hash Resolution", () => {
test("should resolve valid 13-char Crockford Base32 hash", async () => {
const uwf = await makeUwfStore(storageRoot);
const hash = await storeWorkflow(uwf, "test-workflow");
const result = await cmdThreadStart(storageRoot, hash, "test prompt", projectRoot);
expect(result.workflow).toBe(hash);
expect(result.thread).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
});
test("should fail on invalid hash format (non-Crockford characters)", async () => {
await makeUwfStore(storageRoot);
await expect(
cmdThreadStart(storageRoot, "123456789ABCD", "prompt", projectRoot),
).rejects.toThrow();
});
test("should fail on valid-format hash not present in CAS", async () => {
await makeUwfStore(storageRoot);
const fakeHash = "0000000000000"; // valid format, doesn't exist
await expect(cmdThreadStart(storageRoot, fakeHash, "prompt", projectRoot)).rejects.toThrow();
});
test("should reject 40-char hex hash (legacy format not supported)", async () => {
await makeUwfStore(storageRoot);
const hexHash = "a".repeat(40);
await expect(cmdThreadStart(storageRoot, hexHash, "prompt", projectRoot)).rejects.toThrow();
});
});
// ── Strategy 2: File Path Resolution ──────────────────────────────────────────
describe("Strategy 2: File Path Resolution", () => {
test("should load workflow from absolute file path", async () => {
await makeUwfStore(storageRoot);
const yamlPath = join(tmpDir, "test-workflow.yaml");
await writeFile(yamlPath, await createWorkflowYaml("test-workflow"));
const result = await cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("test-workflow");
}
});
test("should load workflow from relative file path", async () => {
await makeUwfStore(storageRoot);
const yamlPath = "test-workflow.yaml";
await writeFile(join(projectRoot, yamlPath), await createWorkflowYaml("test-workflow"));
const result = await cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should fail when file path does not exist", async () => {
await makeUwfStore(storageRoot);
await expect(
cmdThreadStart(storageRoot, "./nonexistent.yaml", "prompt", projectRoot),
).rejects.toThrow();
});
test("should fail on invalid YAML syntax in file", async () => {
await makeUwfStore(storageRoot);
const yamlPath = join(tmpDir, "bad-syntax.yaml");
await writeFile(yamlPath, "invalid: yaml: : :");
await expect(cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot)).rejects.toThrow();
});
test("should fail on valid YAML with invalid WorkflowPayload shape", async () => {
await makeUwfStore(storageRoot);
const yamlPath = join(tmpDir, "invalid-workflow.yaml");
await writeFile(yamlPath, "name: test\n# missing roles, conditions, and graph");
await expect(cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot)).rejects.toThrow();
});
test("should enforce filename matches workflow name", async () => {
await makeUwfStore(storageRoot);
const yamlPath = join(tmpDir, "solve-issue.yaml");
await writeFile(yamlPath, await createWorkflowYaml("wrong-name"));
await expect(cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot)).rejects.toThrow();
});
});
// ── Strategy 3: Local Discovery (Parent Traversal) ────────────────────────────
describe("Strategy 3: Local Discovery", () => {
test("should find workflow in current directory .workflow/", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
}
});
test("should find workflow in parent directory .workflow/", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
const subdir = join(projectRoot, "packages", "cli-workflow", "src");
await mkdir(subdir, { recursive: true });
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", subdir);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should stop at filesystem root when traversing", async () => {
await makeUwfStore(storageRoot);
const deepPath = join(tmpDir, "deep", "path", "that", "does", "not", "have", "workflow");
await mkdir(deepPath, { recursive: true });
await expect(cmdThreadStart(storageRoot, "nonexistent", "prompt", deepPath)).rejects.toThrow();
});
test("should prefer .workflow/ over .workflows/ directory", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow");
const workflowsDir = join(projectRoot, ".workflows");
await mkdir(workflowDir, { recursive: true });
await mkdir(workflowsDir, { recursive: true });
await writeFile(
join(workflowDir, "solve-issue.yaml"),
await createWorkflowYaml("solve-issue", "1"),
);
await writeFile(
join(workflowsDir, "solve-issue.yaml"),
await createWorkflowYaml("solve-issue", "2"),
);
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (1)");
}
});
test("should support .yml extension in local discovery", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
await writeFile(join(workflowDir, "solve-issue.yml"), await createWorkflowYaml("solve-issue"));
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
});
// ── Strategy 4: Global Registry Fallback ──────────────────────────────────────
describe("Strategy 4: Global Registry Resolution", () => {
test("should resolve workflow from global registry when not found locally", async () => {
const uwf = await makeUwfStore(storageRoot);
const hash = await storeWorkflow(uwf, "deploy-pipeline");
const registry = await loadWorkflowRegistry(storageRoot);
registry["deploy-pipeline"] = hash;
await saveWorkflowRegistry(storageRoot, registry);
const isolatedRoot = join(tmpDir, "isolated");
await mkdir(isolatedRoot, { recursive: true });
const result = await cmdThreadStart(storageRoot, "deploy-pipeline", "prompt", isolatedRoot);
expect(result.workflow).toBe(hash);
});
test("should fail when workflow not found in any strategy", async () => {
await makeUwfStore(storageRoot);
await expect(cmdThreadStart(storageRoot, "nonexistent", "prompt", tmpDir)).rejects.toThrow();
});
});
// ── Strategy Priority Order ───────────────────────────────────────────────────
describe("Resolution Priority", () => {
test("should use explicit file path over local discovery", async () => {
await makeUwfStore(storageRoot);
// Setup: Create workflow in .workflow/ AND as explicit file
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
await writeFile(
join(workflowDir, "solve-issue.yaml"),
await createWorkflowYaml("solve-issue", "discovery"),
);
const explicitPath = join(projectRoot, "custom-solve-issue.yaml");
await writeFile(explicitPath, await createWorkflowYaml("custom-solve-issue", "explicit"));
// Execute with explicit path
const result = await cmdThreadStart(storageRoot, explicitPath, "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (explicit)");
}
});
test("should use local discovery over global registry", async () => {
const uwf = await makeUwfStore(storageRoot);
// Setup: Register globally
const globalHash = await storeWorkflow(uwf, "solve-issue");
const registry = await loadWorkflowRegistry(storageRoot);
registry["solve-issue"] = globalHash;
await saveWorkflowRegistry(storageRoot, registry);
// Setup: Create local .workflow/
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
const localYaml = await createWorkflowYaml("solve-issue", "local");
await writeFile(join(workflowDir, "solve-issue.yaml"), localYaml);
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf2 = await makeUwfStore(storageRoot);
const node = uwf2.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (local)");
}
});
});
// ── Edge Cases ────────────────────────────────────────────────────────────────
describe("Edge Cases", () => {
test("should treat '13-char-string.yaml' as file path, not CAS hash", async () => {
await makeUwfStore(storageRoot);
const fileName = "0123456789ABC.yaml"; // 13 chars + .yaml
await writeFile(join(projectRoot, fileName), await createWorkflowYaml("0123456789ABC"));
const result = await cmdThreadStart(storageRoot, fileName, "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should handle workflow names containing slashes as file paths", async () => {
await makeUwfStore(storageRoot);
const filePath = "subdir/solve-issue.yaml";
const fullPath = join(projectRoot, filePath);
await mkdir(join(projectRoot, "subdir"), { recursive: true });
await writeFile(fullPath, await createWorkflowYaml("solve-issue"));
const result = await cmdThreadStart(storageRoot, filePath, "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should handle absolute paths correctly", async () => {
await makeUwfStore(storageRoot);
const absPath = join(tmpDir, "abs-workflow.yaml");
await writeFile(absPath, await createWorkflowYaml("abs-workflow"));
const result = await cmdThreadStart(storageRoot, absPath, "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should fail on empty workflow ID", async () => {
await makeUwfStore(storageRoot);
await expect(cmdThreadStart(storageRoot, "", "prompt", projectRoot)).rejects.toThrow();
});
test("should fail on whitespace-only workflow ID", async () => {
await makeUwfStore(storageRoot);
await expect(cmdThreadStart(storageRoot, " ", "prompt", projectRoot)).rejects.toThrow();
});
});
@@ -137,6 +137,75 @@ function apiKeyEnvName(providerName: string): string {
return `${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
}
/**
* Discover uwf-* agent binaries in PATH.
* Returns sorted list of binary names (e.g., ["uwf-hermes", "uwf-claude-code"]).
*/
async function _discoverAgents(): Promise<string[]> {
try {
// Use which -a to find all uwf-* binaries in PATH
const proc = Bun.spawn(["which", "-a", "uwf-hermes", "uwf-claude-code", "uwf-cursor"], {
stdout: "pipe",
stderr: "pipe",
});
const text = await new Response(proc.stdout).text();
await proc.exited;
if (proc.exitCode !== 0) {
// Try alternative approach: search PATH directories manually
const pathEnv = process.env.PATH || "";
const pathDirs = pathEnv.split(":").filter((d) => d.length > 0);
const agents = new Set<string>();
for (const dir of pathDirs) {
try {
if (!existsSync(dir)) continue;
const { readdirSync, statSync } = await import("node:fs");
const entries = readdirSync(dir);
for (const entry of entries) {
if (!entry.startsWith("uwf-") || entry === "uwf") continue;
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
// Check if executable (owner, group, or other has execute bit)
if (stat.isFile() && (stat.mode & 0o111) !== 0) {
agents.add(entry);
}
} catch {
// Skip if can't stat
}
}
} catch {
// Skip inaccessible directories
}
}
return Array.from(agents).sort();
}
// Parse which output - each line is a path to a binary
const paths = text
.trim()
.split("\n")
.filter((line) => line.length > 0);
const agents = new Set<string>();
for (const path of paths) {
const basename = path.split("/").pop();
if (basename?.startsWith("uwf-") && basename !== "uwf") {
agents.add(basename);
}
}
return Array.from(agents).sort();
} catch {
// If all fails, return empty array
return [];
}
}
/**
* Merge setup args into config.yaml structure. Non-destructive — preserves existing entries.
*/
+111 -12
View File
@@ -1,5 +1,6 @@
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import { access, readFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
import type { Store as CasStore, JSONSchema } from "@uncaged/json-cas";
import { getSchema, validate } from "@uncaged/json-cas";
import { getEnvPath, loadWorkflowConfig } from "@uncaged/workflow-agent-kit";
@@ -30,12 +31,10 @@ import { parse, stringify } from "yaml";
import {
appendThreadHistory,
createUwfStore,
discoverProjectWorkflows,
findThreadInHistory,
loadThreadHistory,
loadThreadsIndex,
loadWorkflowRegistry,
resolveProjectWorkflowFile,
resolveWorkflowHash,
saveThreadsIndex,
type ThreadHistoryLine,
@@ -82,6 +81,83 @@ function fail(message: string): never {
process.exit(1);
}
/**
* Check if a string looks like a file path (contains path separators or has .yaml/.yml extension).
*/
function isFilePath(input: string): boolean {
return (
input.includes("/") || input.includes("\\") || input.endsWith(".yaml") || input.endsWith(".yml")
);
}
/**
* Check if a workflow file exists at the given path.
*/
async function workflowFileExists(dir: string, name: string, ext: string): Promise<string | null> {
const candidate = resolvePath(dir, `${name}${ext}`);
try {
await access(candidate);
return candidate;
} catch {
return null;
}
}
/**
* Search for a workflow file in a given directory (checks both .workflow/ and .workflows/).
*/
async function findWorkflowInDir(dir: string, name: string): Promise<string | null> {
// Check .workflow/ directory first (preferred)
for (const ext of [".yaml", ".yml"]) {
const result = await workflowFileExists(resolvePath(dir, ".workflow"), name, ext);
if (result !== null) {
return result;
}
}
// Check .workflows/ directory as fallback (legacy)
for (const ext of [".yaml", ".yml"]) {
const result = await workflowFileExists(resolvePath(dir, ".workflows"), name, ext);
if (result !== null) {
return result;
}
}
return null;
}
/**
* Traverse parent directories looking for `.workflow/<name>.yaml` or `.workflow/<name>.yml`.
* Returns the absolute path if found, otherwise null.
* Stops at filesystem root or .git directory.
*/
async function findWorkflowInParents(startDir: string, name: string): Promise<string | null> {
let currentDir = resolvePath(startDir);
const root = resolvePath("/");
while (true) {
const found = await findWorkflowInDir(currentDir, name);
if (found !== null) {
return found;
}
// Stop at filesystem root
if (currentDir === root) {
break;
}
// Move to parent directory
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
// Reached filesystem root
break;
}
currentDir = parentDir;
}
return null;
}
async function materializeLocalWorkflow(uwf: UwfStore, filePath: string): Promise<CasRef> {
let text: string;
try {
@@ -123,18 +199,41 @@ async function resolveWorkflowCasRef(
workflowId: string,
projectRoot: string,
): Promise<CasRef> {
// Project-local resolution: check .workflows/<workflowId>.yaml first
const localEntries = await discoverProjectWorkflows(projectRoot);
const localFile = resolveProjectWorkflowFile(localEntries, workflowId);
if (localFile !== null) {
return materializeLocalWorkflow(uwf, localFile);
// Validate input
const trimmed = workflowId.trim();
if (trimmed === "") {
fail("workflow ID cannot be empty");
}
// Global registry fallback
// Strategy 1: Direct CAS hash
if (isCasRef(trimmed)) {
const node = uwf.store.get(trimmed);
if (node === null) {
fail(`CAS node not found: ${trimmed}`);
}
if (node.type !== uwf.schemas.workflow) {
fail(`node ${trimmed} is not a Workflow (type ${node.type})`);
}
return trimmed;
}
// Strategy 2: Explicit file path (relative or absolute)
if (isFilePath(trimmed)) {
const absolutePath = isAbsolute(trimmed) ? trimmed : resolvePath(projectRoot, trimmed);
return materializeLocalWorkflow(uwf, absolutePath);
}
// Strategy 3: Local discovery (parent directory traversal)
const localPath = await findWorkflowInParents(projectRoot, trimmed);
if (localPath !== null) {
return materializeLocalWorkflow(uwf, localPath);
}
// Strategy 4: Global registry fallback
const registry = await loadWorkflowRegistry(storageRoot);
const hash = resolveWorkflowHash(registry, workflowId);
const hash = resolveWorkflowHash(registry, trimmed);
if (!isCasRef(hash)) {
fail(`workflow not found: ${workflowId}`);
fail(`workflow not found: ${trimmed}`);
}
const node = uwf.store.get(hash);
if (node === null) {
@@ -440,7 +539,7 @@ function collectOrderedSteps(
}
function formatYaml(value: unknown): string {
return stringify(value).trimEnd();
return stringify(value, { aliasDuplicateObjects: false }).trimEnd();
}
function formatCompactStep(index: number, item: OrderedStepItem, outputYaml: string): string {
+1 -1
View File
@@ -7,6 +7,6 @@ export function formatOutput(data: unknown, format: OutputFormat): string {
case "json":
return JSON.stringify(data);
case "yaml":
return stringify(data).trimEnd();
return stringify(data, { aliasDuplicateObjects: false }).trimEnd();
}
}
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import type { Store } from "@uncaged/json-cas";
import {
type AgentContext,
@@ -49,6 +50,7 @@ export function buildClaudeCodePrompt(ctx: AgentContext): string {
if (historyBlock !== "") {
parts.push("", historyBlock);
}
parts.push("", "## Current Instruction", "", ctx.edgePrompt);
return parts.join("\n");
}
@@ -116,7 +118,17 @@ function spawnClaudeResume(
]);
}
const NDJSON_DUMP_DIR = "/tmp/uwf-ndjson-dump";
async function processClaudeOutput(stdout: string, store: Store): Promise<AgentRunResult> {
// Debug dump: save raw NDJSON for issue #439 investigation
try {
mkdirSync(NDJSON_DUMP_DIR, { recursive: true });
writeFileSync(`${NDJSON_DUMP_DIR}/${Date.now()}.ndjson`, stdout);
} catch {
// ignore dump failures
}
const parsed = parseClaudeCodeStreamOutput(stdout);
if (parsed !== null) {
@@ -132,6 +144,8 @@ async function processClaudeOutput(stdout: string, store: Store): Promise<AgentR
async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
const fullPrompt = buildClaudeCodePrompt(ctx);
log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`);
// Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry).
if (!ctx.isFirstVisit) {
const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role);