diff --git a/packages/software-factory/prompts/system.md b/packages/software-factory/prompts/system.md index bef657082a..e75cbf16f0 100644 --- a/packages/software-factory/prompts/system.md +++ b/packages/software-factory/prompts/system.md @@ -23,7 +23,6 @@ inspect existing state before making changes — do not guess. # Realms - Target realm: {{targetRealmUrl}} -- Test realm: {{testRealmUrl}} {{#each skills}} diff --git a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts index aa095dbf78..23349fff72 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts @@ -114,7 +114,6 @@ async function main(): Promise { skills: [], tools: [], targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-tests/', }; log.info('Sending plan() request...'); diff --git a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts index 01b7d2ce18..44962ffee0 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts @@ -16,9 +16,9 @@ import { parseArgs } from 'node:util'; import { logger } from '../../src/logger'; import type { - KnowledgeArticle, - ProjectCard, - IssueCard, + KnowledgeArticleData, + ProjectData, + IssueData, } from '../../src/factory-agent'; import { ContextBuilder } from '../../src/factory-context-builder'; import { @@ -50,12 +50,12 @@ function check(label: string, ok: boolean, detail?: string): void { // Fixtures // --------------------------------------------------------------------------- -const SAMPLE_PROJECT: ProjectCard = { +const SAMPLE_PROJECT: ProjectData = { id: 'Projects/sticky-notes', name: 'Sticky Notes MVP', }; -const SAMPLE_KNOWLEDGE: KnowledgeArticle[] = [ +const SAMPLE_KNOWLEDGE: KnowledgeArticleData[] = [ { id: 'Knowledge/card-basics', title: 'Boxel Card Development Basics', @@ -68,7 +68,7 @@ const SAMPLE_KNOWLEDGE: KnowledgeArticle[] = [ }, ]; -const SAMPLE_ISSUES: { label: string; issue: IssueCard }[] = [ +const SAMPLE_ISSUES: { label: string; issue: IssueData }[] = [ { label: 'Card definition (.gts work)', issue: { @@ -152,7 +152,6 @@ async function main(): Promise { issue, knowledge: SAMPLE_KNOWLEDGE, targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-test-artifacts/', }); log.info(' First pass (no test results):'); @@ -172,10 +171,6 @@ async function main(): Promise { 'targetRealmUrl set', ctx.targetRealmUrl === 'https://example.test/user/target/', ); - check( - 'testRealmUrl set', - ctx.testRealmUrl === 'https://example.test/user/target-test-artifacts/', - ); let totalTokens = ctx.skills.reduce((s, sk) => s + estimateTokens(sk), 0); log.info(` Skill breakdown (~${totalTokens} total tokens):`); @@ -195,7 +190,6 @@ async function main(): Promise { issue, knowledge: SAMPLE_KNOWLEDGE, targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-test-artifacts/', testResults: { status: 'failed', passedCount: 2, @@ -254,7 +248,6 @@ async function main(): Promise { issue: SAMPLE_ISSUES[0].issue, knowledge: [], targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-test-artifacts/', }); let totalTokens = ctx.skills.reduce((s, sk) => s + estimateTokens(sk), 0); diff --git a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts index 28d737357e..207ad2675e 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts @@ -17,10 +17,10 @@ import { logger } from '../../src/logger'; import type { AgentContext, - KnowledgeArticle, - ProjectCard, + KnowledgeArticleData, + ProjectData, TestResult, - IssueCard, + IssueData, } from '../../src/factory-agent'; import type { @@ -118,11 +118,10 @@ class MockLoopAgent implements LoopAgent { class StubContextBuilder implements ContextBuilderLike { async build(params: { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; testResults?: TestResult; }): Promise { return { @@ -131,7 +130,6 @@ class StubContextBuilder implements ContextBuilderLike { knowledge: params.knowledge, skills: [], targetRealmUrl: params.targetRealmUrl, - testRealmUrl: params.testRealmUrl, testResults: params.testResults, }; } @@ -141,18 +139,18 @@ class StubContextBuilder implements ContextBuilderLike { // Fixtures // --------------------------------------------------------------------------- -const PROJECT: ProjectCard = { +const PROJECT: ProjectData = { id: 'Projects/sticky-notes', name: 'Sticky Notes MVP', }; -const ISSUE: IssueCard = { +const ISSUE: IssueData = { id: 'Issues/define-sticky-note', title: 'Define StickyNote card', description: 'Create a .gts card definition for StickyNote.', }; -const KNOWLEDGE: KnowledgeArticle[] = [ +const KNOWLEDGE: KnowledgeArticleData[] = [ { id: 'Knowledge/card-basics', title: 'Boxel Card Development Basics' }, ]; @@ -198,7 +196,6 @@ function makeBaseConfig( issue: ISSUE, knowledge: KNOWLEDGE, targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', ...overrides, }; } diff --git a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts index 33c72b838a..4f04ecefe3 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts @@ -108,7 +108,6 @@ const SAMPLE_CONTEXT: AgentContext = { }, ], targetRealmUrl: 'http://localhost:4201/user/personal/', - testRealmUrl: 'http://localhost:4201/user/personal-tests/', }; const SAMPLE_PREVIOUS_ACTIONS: AgentAction[] = [ diff --git a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts index e1d8d0b2f0..1860de5153 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -21,9 +21,9 @@ import { enforceSkillBudget, estimateTokens, } from '../../src/factory-skill-loader'; -import type { ProjectCard, IssueCard } from '../../src/factory-agent'; +import type { ProjectData, IssueData } from '../../src/factory-agent'; -const SAMPLE_ISSUES: { label: string; issue: IssueCard }[] = [ +const SAMPLE_ISSUES: { label: string; issue: IssueData }[] = [ { label: 'Generic card work (base case)', issue: { @@ -94,7 +94,7 @@ async function main(): Promise { let resolver = new DefaultSkillResolver(); let loader = new SkillLoader(); - let project: ProjectCard = { id: 'Projects/smoke-test' }; + let project: ProjectData = { id: 'Projects/smoke-test' }; log.info('=== Skill Loader & Resolver Smoke Test ===\n'); @@ -107,7 +107,7 @@ async function main(): Promise { id: 'Issues/custom', title: customIssueText, description: customIssueText, - } as IssueCard, + } as IssueData, }, ] : SAMPLE_ISSUES; diff --git a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts index ed2ac2a557..81c471572d 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts @@ -137,7 +137,6 @@ async function main(): Promise { let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', sourceRealmUrl: 'https://realms.example.test/user/source/', allowedRealmPrefixes: ['https://realms.example.test/user/scratch-'], }); @@ -184,7 +183,6 @@ async function main(): Promise { let mockExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', fetch: (async (input: RequestInfo | URL, init?: RequestInit) => { mockCallCount++; let url = String(input); @@ -251,7 +249,6 @@ async function main(): Promise { let toolBuilderExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', fetch: toolBuilderFetch, }); diff --git a/packages/software-factory/src/factory-agent-tool-use.ts b/packages/software-factory/src/factory-agent-tool-use.ts index 51ccacc99a..cb2222c3c1 100644 --- a/packages/software-factory/src/factory-agent-tool-use.ts +++ b/packages/software-factory/src/factory-agent-tool-use.ts @@ -314,7 +314,6 @@ export class ToolUseFactoryAgent implements LoopAgent { return this.promptLoader.load('system', { targetRealmUrl: context.targetRealmUrl, - testRealmUrl: context.testRealmUrl, skills, }); } diff --git a/packages/software-factory/src/factory-agent-types.ts b/packages/software-factory/src/factory-agent-types.ts index 97395cbd33..e9f5d14768 100644 --- a/packages/software-factory/src/factory-agent-types.ts +++ b/packages/software-factory/src/factory-agent-types.ts @@ -61,17 +61,17 @@ export interface FactoryAgentConfig { debug?: boolean; } -export interface ProjectCard { +export interface ProjectData { id: string; [key: string]: unknown; } -export interface IssueCard { +export interface IssueData { id: string; [key: string]: unknown; } -export interface KnowledgeArticle { +export interface KnowledgeArticleData { id: string; [key: string]: unknown; } @@ -111,6 +111,38 @@ export interface TestResult { durationMs: number; } +// --------------------------------------------------------------------------- +// Validation types (broader than TestResult) +// --------------------------------------------------------------------------- + +/** Steps in the post-iteration validation pipeline. */ +export type ValidationStep = + | 'parse' + | 'lint' + | 'evaluate' + | 'instantiate' + | 'test'; + +export interface ValidationError { + file?: string; + message: string; + stackTrace?: string; +} + +/** Result of a single validation step. */ +export interface ValidationStepResult { + step: ValidationStep; + passed: boolean; + files?: string[]; + errors: ValidationError[]; +} + +/** Aggregated results from a full validation run (all steps). */ +export interface ValidationResults { + passed: boolean; + steps: ValidationStepResult[]; +} + export interface ToolResult { tool: string; exitCode: number; @@ -119,9 +151,9 @@ export interface ToolResult { } export interface AgentContext { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; skills: ResolvedSkill[]; /** @deprecated Tools are now provided separately as FactoryTool[] to agent.run(). */ tools?: ToolManifest[]; @@ -133,7 +165,10 @@ export interface AgentContext { /** @deprecated Iteration tracking is now owned by the orchestrator. */ iteration?: number; targetRealmUrl: string; - testRealmUrl: string; + /** Validation results from the prior inner-loop iteration. */ + validationResults?: ValidationResults; + /** Brief URL for bootstrap issues. */ + briefUrl?: string; } export interface AgentAction { diff --git a/packages/software-factory/src/factory-context-builder.ts b/packages/software-factory/src/factory-context-builder.ts index 86882d6e74..9d2589d5c9 100644 --- a/packages/software-factory/src/factory-context-builder.ts +++ b/packages/software-factory/src/factory-context-builder.ts @@ -1,9 +1,10 @@ import type { AgentContext, - IssueCard, - KnowledgeArticle, - ProjectCard, + IssueData, + KnowledgeArticleData, + ProjectData, TestResult, + ValidationResults, } from './factory-agent'; import type { ResolvedSkill } from './factory-agent'; @@ -14,6 +15,22 @@ import { type SkillResolver, } from './factory-skill-loader'; +// --------------------------------------------------------------------------- +// Issue relationship loader +// --------------------------------------------------------------------------- + +/** + * Loads related cards from an issue's relationships. + * + * The `buildForIssue()` method uses this to traverse the issue's + * linksTo / linksToMany fields (project, relatedKnowledge) + * without coupling ContextBuilder to the realm I/O layer. + */ +export interface IssueRelationshipLoader { + loadProject(issue: IssueData): Promise; + loadKnowledge(issue: IssueData): Promise; +} + // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- @@ -23,6 +40,8 @@ export interface ContextBuilderConfig { skillLoader: SkillLoaderInterface; /** Maximum token budget for skills. When set, enforceSkillBudget() trims. */ maxSkillTokens?: number; + /** Loader for traversing issue relationships (required for buildForIssue). */ + issueLoader?: IssueRelationshipLoader; } // --------------------------------------------------------------------------- @@ -33,11 +52,13 @@ export class ContextBuilder { private skillResolver: SkillResolver; private skillLoader: SkillLoaderInterface; private maxSkillTokens: number | undefined; + private issueLoader: IssueRelationshipLoader | undefined; constructor(config: ContextBuilderConfig) { this.skillResolver = config.skillResolver; this.skillLoader = config.skillLoader; this.maxSkillTokens = config.maxSkillTokens; + this.issueLoader = config.issueLoader; } /** @@ -50,15 +71,14 @@ export class ContextBuilder { * 4. Return AgentContext (tools are provided separately as FactoryTool[]) */ async build(params: { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; /** Test results from the previous iteration, if any. */ testResults?: TestResult; }): Promise { - let { project, issue, knowledge, targetRealmUrl, testRealmUrl } = params; + let { project, issue, knowledge, targetRealmUrl } = params; // Step 1: Resolve which skills are needed for this issue let skillNames = this.skillResolver.resolve(issue, project); @@ -79,7 +99,6 @@ export class ContextBuilder { knowledge, skills, targetRealmUrl, - testRealmUrl, }; // Include test results when iterating after a failed test run @@ -89,4 +108,71 @@ export class ContextBuilder { return context; } + + /** + * Build agent context from the current issue (issue-driven loop). + * + * Unlike `build()` which takes pre-loaded project/knowledge, this method + * traverses issue relationships to load them automatically: + * - project from issue.project + * - knowledge from issue.relatedKnowledge + * + * Accepts optional validationResults from the prior inner-loop iteration + * so the agent can self-correct on failures. + */ + async buildForIssue(params: { + issue: IssueData; + targetRealmUrl: string; + validationResults?: ValidationResults; + briefUrl?: string; + }): Promise { + if (!this.issueLoader) { + throw new Error( + 'buildForIssue() requires an issueLoader in ContextBuilderConfig', + ); + } + + let { issue, targetRealmUrl } = params; + + // Step 1: Traverse issue relationships + let [project, knowledge] = await Promise.all([ + this.issueLoader.loadProject(issue), + this.issueLoader.loadKnowledge(issue), + ]); + + if (!project) { + throw new Error( + `Issue "${issue.id}" has no linked project — cannot build context`, + ); + } + + // Step 2: Resolve and load skills + let skillNames = this.skillResolver.resolve(issue, project); + let skills: ResolvedSkill[] = await this.skillLoader.loadAll( + skillNames, + issue, + ); + + // Step 3: Enforce token budget if configured + skills = enforceSkillBudget(skills, this.maxSkillTokens); + + // Step 4: Assemble the context + let context: AgentContext = { + project, + issue, + knowledge, + skills, + targetRealmUrl, + }; + + if (params.validationResults) { + context.validationResults = params.validationResults; + } + + if (params.briefUrl) { + context.briefUrl = params.briefUrl; + } + + return context; + } } diff --git a/packages/software-factory/src/factory-implement.ts b/packages/software-factory/src/factory-implement.ts index 9e42bf7a85..5303f9ecf6 100644 --- a/packages/software-factory/src/factory-implement.ts +++ b/packages/software-factory/src/factory-implement.ts @@ -18,9 +18,9 @@ import { resolve } from 'node:path'; import { logger } from './logger'; import type { - IssueCard, - KnowledgeArticle, - ProjectCard, + IssueData, + KnowledgeArticleData, + ProjectData, TestResult, } from './factory-agent'; import { @@ -137,10 +137,6 @@ export async function runFactoryImplement( let realmServerUrl = ensureTrailingSlash(config.realmServerUrl); let fetchImpl = config.fetch ?? globalThis.fetch; - // Derive the test-artifacts realm URL from the target realm URL. - // e.g., "http://localhost:4201/user/my-realm/" -> "http://localhost:4201/user/my-realm-test-artifacts/" - let testRealmUrl = targetRealmUrl.replace(/\/$/, '-test-artifacts/'); - // 1. Auth: get Matrix auth, server token, and per-realm JWTs let { serverToken, realmTokens } = await resolveAuth(config); @@ -160,7 +156,6 @@ export async function runFactoryImplement( let toolExecutor = new ToolExecutor(toolRegistry, { packageRoot: PACKAGE_ROOT, targetRealmUrl, - testRealmUrl, fetch: fetchImpl, authorization: config.authorization, }); @@ -244,7 +239,6 @@ export async function runFactoryImplement( issue, knowledge, targetRealmUrl, - testRealmUrl, maxIterations: config.maxIterations, }); @@ -383,9 +377,9 @@ async function fetchCardData( bootstrapResult: FactoryBootstrapResult, fetchOptions: RealmFetchOptions, ): Promise<{ - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; }> { // Fetch the project card let project = await fetchCard( @@ -402,7 +396,7 @@ async function fetchCardData( ); // Fetch all knowledge articles - let knowledge: KnowledgeArticle[] = []; + let knowledge: KnowledgeArticleData[] = []; for (let ka of bootstrapResult.knowledgeArticles) { try { let card = await fetchCard(targetRealmUrl, ka.id, fetchOptions); @@ -461,7 +455,7 @@ interface TestRunnerConfig { */ function buildTestRunner( targetRealmUrl: string, - issue: IssueCard, + issue: IssueData, toolCallLog: ToolCallEntry[], runConfig: TestRunnerConfig, ): TestRunner { diff --git a/packages/software-factory/src/factory-loop.ts b/packages/software-factory/src/factory-loop.ts index 8451493bcc..682b3c2ac8 100644 --- a/packages/software-factory/src/factory-loop.ts +++ b/packages/software-factory/src/factory-loop.ts @@ -20,9 +20,9 @@ import type { AgentContext, - IssueCard, - KnowledgeArticle, - ProjectCard, + IssueData, + KnowledgeArticleData, + ProjectData, TestResult, } from './factory-agent'; @@ -73,11 +73,10 @@ export type TestRunner = () => Promise; */ export interface ContextBuilderLike { build(params: { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; testResults?: TestResult; }): Promise; } @@ -87,11 +86,10 @@ export interface FactoryLoopConfig { contextBuilder: ContextBuilderLike; tools: FactoryTool[]; testRunner: TestRunner; - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; /** Maximum iterations before the loop gives up. Default: 5. */ maxIterations?: number; } @@ -129,7 +127,6 @@ export async function runFactoryLoop( issue: config.issue, knowledge: config.knowledge, targetRealmUrl: config.targetRealmUrl, - testRealmUrl: config.testRealmUrl, testResults, }); diff --git a/packages/software-factory/src/factory-prompt-loader.ts b/packages/software-factory/src/factory-prompt-loader.ts index 992ab8dddf..cfd55f1633 100644 --- a/packages/software-factory/src/factory-prompt-loader.ts +++ b/packages/software-factory/src/factory-prompt-loader.ts @@ -386,7 +386,6 @@ export function assembleSystemPrompt( return loader.load('system', { targetRealmUrl: context.targetRealmUrl, - testRealmUrl: context.testRealmUrl, skills, }); } diff --git a/packages/software-factory/src/factory-skill-loader.ts b/packages/software-factory/src/factory-skill-loader.ts index 53cd9be5b5..be946c0b72 100644 --- a/packages/software-factory/src/factory-skill-loader.ts +++ b/packages/software-factory/src/factory-skill-loader.ts @@ -1,7 +1,7 @@ import { readdir, readFile, stat } from 'node:fs/promises'; import { join, resolve } from 'node:path'; -import type { IssueCard, ProjectCard, ResolvedSkill } from './factory-agent'; +import type { IssueData, ProjectData, ResolvedSkill } from './factory-agent'; // --------------------------------------------------------------------------- // Constants @@ -132,7 +132,7 @@ interface RawSkillData { // --------------------------------------------------------------------------- export interface SkillResolver { - resolve(issue: IssueCard, project: ProjectCard): string[]; + resolve(issue: IssueData, project: ProjectData): string[]; } export class DefaultSkillResolver implements SkillResolver { @@ -150,7 +150,7 @@ export class DefaultSkillResolver implements SkillResolver { * tool registry does not include boxel-cli tools (deferred to CS-10520). * These skills reference commands the agent cannot invoke. */ - resolve(issue: IssueCard, project: ProjectCard): string[] { + resolve(issue: IssueData, project: ProjectData): string[] { let issueText = extractIssueText(issue); let skills: string[] = ['boxel-development', 'boxel-file-structure']; @@ -182,8 +182,8 @@ export class DefaultSkillResolver implements SkillResolver { // --------------------------------------------------------------------------- export interface SkillLoaderInterface { - load(skillName: string, issue?: IssueCard): Promise; - loadAll(skillNames: string[], issue?: IssueCard): Promise; + load(skillName: string, issue?: IssueData): Promise; + loadAll(skillNames: string[], issue?: IssueData): Promise; } export class SkillLoader implements SkillLoaderInterface { @@ -210,7 +210,7 @@ export class SkillLoader implements SkillLoaderInterface { * only include issue-relevant files (always applied, not just with a budget). * Results are cached for the duration of the factory run. */ - async load(skillName: string, issue?: IssueCard): Promise { + async load(skillName: string, issue?: IssueData): Promise { let raw = await this.loadRaw(skillName); return toResolvedSkill(raw, issue); } @@ -221,7 +221,7 @@ export class SkillLoader implements SkillLoaderInterface { */ async loadAll( skillNames: string[], - issue?: IssueCard, + issue?: IssueData, ): Promise { let results: ResolvedSkill[] = []; @@ -415,7 +415,7 @@ export function estimateTokens(skill: ResolvedSkill): number { * using actual filenames — this happens on every load, not just when a * budget is enforced. */ -function toResolvedSkill(raw: RawSkillData, issue?: IssueCard): ResolvedSkill { +function toResolvedSkill(raw: RawSkillData, issue?: IssueData): ResolvedSkill { let refs = raw.references; if (refs && raw.name === 'boxel-development' && issue) { @@ -439,7 +439,7 @@ function toResolvedSkill(raw: RawSkillData, issue?: IssueCard): ResolvedSkill { */ function filterBoxelDevelopmentRefs( refs: NamedReference[], - issue: IssueCard, + issue: IssueData, ): NamedReference[] { let issueText = extractIssueText(issue); @@ -474,7 +474,7 @@ function filterBoxelDevelopmentRefs( * Concatenates known text fields (id, title, description, tags, labels, etc.) * into a single lowercase string for keyword matching. */ -export function extractIssueText(issue: IssueCard): string { +export function extractIssueText(issue: IssueData): string { let parts: string[] = [issue.id]; for (let key of [ @@ -518,8 +518,8 @@ export function extractIssueText(issue: IssueCard): string { * generic `knowledge` field for forward compatibility. */ function extractKnowledgeSkillTags( - project: ProjectCard, - issue?: IssueCard, + project: ProjectData, + issue?: IssueData, ): string[] { let articles: unknown[] = []; diff --git a/packages/software-factory/src/factory-tool-executor.ts b/packages/software-factory/src/factory-tool-executor.ts index fc7f66beba..daab6729c1 100644 --- a/packages/software-factory/src/factory-tool-executor.ts +++ b/packages/software-factory/src/factory-tool-executor.ts @@ -51,8 +51,6 @@ export interface ToolExecutorConfig { packageRoot: string; /** Target realm URL — tools may only target this realm. */ targetRealmUrl: string; - /** Test realm URL — tools may also target this realm. */ - testRealmUrl: string; /** Additional scratch realm URL prefixes that are allowed. */ allowedRealmPrefixes?: string[]; /** Source realm URL — tools must NEVER target this realm. */ @@ -280,10 +278,9 @@ export class ToolExecutor { private validateRealmTarget(toolName: string, realmUrl: string): void { let normalized = ensureTrailingSlash(realmUrl); let target = ensureTrailingSlash(this.config.targetRealmUrl); - let test = ensureTrailingSlash(this.config.testRealmUrl); // Exact realm matches (with trailing slash normalization) - let exactAllowed = [target, test]; + let exactAllowed = [target]; // Prefix matches (no trailing slash — these are URL path prefixes) let prefixAllowed = this.config.allowedRealmPrefixes ?? []; @@ -320,11 +317,6 @@ export class ToolExecutor { } catch { // skip invalid } - try { - allowedOrigins.add(new URL(this.config.testRealmUrl).origin); - } catch { - // skip invalid - } for (let prefix of this.config.allowedRealmPrefixes ?? []) { try { allowedOrigins.add(new URL(prefix).origin); diff --git a/packages/software-factory/src/test-run-cards.ts b/packages/software-factory/src/test-run-cards.ts index 151329a339..7ad735367b 100644 --- a/packages/software-factory/src/test-run-cards.ts +++ b/packages/software-factory/src/test-run-cards.ts @@ -33,7 +33,7 @@ export async function createTestRun( ); let result = await writeFile( - options.testRealmUrl, + options.targetRealmUrl, `${testRunId}.json`, JSON.stringify(document, null, 2), { authorization: options.authorization, fetch: options.fetch }, @@ -63,7 +63,11 @@ export async function completeTestRun( // may be stale causing the first fetch to fail with "fetch failed". let readResult: Awaited> | undefined; for (let attempt = 0; attempt < 3; attempt++) { - readResult = await readFile(options.testRealmUrl, testRunId, fetchOptions); + readResult = await readFile( + options.targetRealmUrl, + testRunId, + fetchOptions, + ); if (readResult.ok && readResult.document) { break; } @@ -106,7 +110,7 @@ export async function completeTestRun( } let writeResult = await writeFile( - options.testRealmUrl, + options.targetRealmUrl, `${testRunId}.json`, JSON.stringify(readResult.document, null, 2), fetchOptions, diff --git a/packages/software-factory/src/test-run-execution.ts b/packages/software-factory/src/test-run-execution.ts index a1edb15db9..1d4bb948bb 100644 --- a/packages/software-factory/src/test-run-execution.ts +++ b/packages/software-factory/src/test-run-execution.ts @@ -36,7 +36,7 @@ export async function resolveTestRun( options: ExecuteTestRunOptions, ): Promise { let realmOptions: TestRunRealmOptions = { - testRealmUrl: options.targetRealmUrl, + targetRealmUrl: options.targetRealmUrl, testResultsModuleUrl: options.testResultsModuleUrl, authorization: options.authorization, fetch: options.fetch, @@ -89,10 +89,10 @@ export async function resolveTestRun( async function findResumableTestRun( options: TestRunRealmOptions, ): Promise { - let testRealmUrl = ensureTrailingSlash(options.testRealmUrl); + let targetRealmUrl = ensureTrailingSlash(options.targetRealmUrl); let result = await searchRealm( - options.testRealmUrl, + options.targetRealmUrl, { filter: { on: { module: options.testResultsModuleUrl, name: 'TestRun' }, @@ -130,8 +130,8 @@ async function findResumableTestRun( .map((r) => r.testName ?? ''); let cardId = latest.id ?? ''; - let relativePath = cardId.startsWith(testRealmUrl) - ? cardId.slice(testRealmUrl.length) + let relativePath = cardId.startsWith(targetRealmUrl) + ? cardId.slice(targetRealmUrl.length) : cardId; return { @@ -146,7 +146,7 @@ async function getNextSequenceNumber( minSequenceNumber = 0, ): Promise { let result = await searchRealm( - options.testRealmUrl, + options.targetRealmUrl, { filter: { on: { module: options.testResultsModuleUrl, name: 'TestRun' }, @@ -414,7 +414,7 @@ export async function executeTestRunFromRealm( options: ExecuteTestRunOptions, ): Promise { let realmOptions: TestRunRealmOptions = { - testRealmUrl: options.targetRealmUrl, + targetRealmUrl: options.targetRealmUrl, testResultsModuleUrl: options.testResultsModuleUrl, authorization: options.authorization, fetch: options.fetch, diff --git a/packages/software-factory/src/test-run-types.ts b/packages/software-factory/src/test-run-types.ts index b83c2b5405..bc06f9a243 100644 --- a/packages/software-factory/src/test-run-types.ts +++ b/packages/software-factory/src/test-run-types.ts @@ -31,7 +31,7 @@ export interface RunRealmTestsFailure { /** Realm connection options for TestRun card operations. */ export interface TestRunRealmOptions { - testRealmUrl: string; + targetRealmUrl: string; /** URL to the test-results module in the source realm. Required, never inferred. */ testResultsModuleUrl: string; authorization?: string; diff --git a/packages/software-factory/tests/factory-agent.integration.test.ts b/packages/software-factory/tests/factory-agent.integration.test.ts index 73e63e1ea4..57e1b2144a 100644 --- a/packages/software-factory/tests/factory-agent.integration.test.ts +++ b/packages/software-factory/tests/factory-agent.integration.test.ts @@ -21,7 +21,6 @@ function makeMinimalContext(overrides?: Partial): AgentContext { skills: [], tools: [], targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', ...overrides, }; } diff --git a/packages/software-factory/tests/factory-agent.test.ts b/packages/software-factory/tests/factory-agent.test.ts index f69907c663..7f8ccd5869 100644 --- a/packages/software-factory/tests/factory-agent.test.ts +++ b/packages/software-factory/tests/factory-agent.test.ts @@ -35,7 +35,6 @@ function makeMinimalContext(overrides?: Partial): AgentContext { skills: [], tools: [], targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', ...overrides, }; } diff --git a/packages/software-factory/tests/factory-context-builder.test.ts b/packages/software-factory/tests/factory-context-builder.test.ts index 027038abce..fde2114fb5 100644 --- a/packages/software-factory/tests/factory-context-builder.test.ts +++ b/packages/software-factory/tests/factory-context-builder.test.ts @@ -1,16 +1,18 @@ import { module, test } from 'qunit'; import type { - KnowledgeArticle, - ProjectCard, + KnowledgeArticleData, + ProjectData, ResolvedSkill, TestResult, - IssueCard, + ValidationResults, + IssueData, } from '../src/factory-agent'; import { ContextBuilder, type ContextBuilderConfig, + type IssueRelationshipLoader, } from '../src/factory-context-builder'; import type { @@ -26,13 +28,13 @@ class StubSkillResolver implements SkillResolver { /** Pre-configured skill names returned by resolve(). */ skillNames: string[]; /** Records all (issue, project) pairs passed to resolve(). */ - calls: { issue: IssueCard; project: ProjectCard }[] = []; + calls: { issue: IssueData; project: ProjectData }[] = []; constructor(skillNames: string[] = ['boxel-development']) { this.skillNames = skillNames; } - resolve(issue: IssueCard, project: ProjectCard): string[] { + resolve(issue: IssueData, project: ProjectData): string[] { this.calls.push({ issue, project }); return this.skillNames; } @@ -42,13 +44,13 @@ class StubSkillLoader implements SkillLoaderInterface { /** Map from skill name to the ResolvedSkill that load() returns. */ private skillMap: Map; /** Records all loadAll() calls: [skillNames, issue]. */ - loadAllCalls: { skillNames: string[]; issue?: IssueCard }[] = []; + loadAllCalls: { skillNames: string[]; issue?: IssueData }[] = []; constructor(skills: ResolvedSkill[] = []) { this.skillMap = new Map(skills.map((s) => [s.name, s])); } - async load(skillName: string, _issue?: IssueCard): Promise { + async load(skillName: string, _issue?: IssueData): Promise { let skill = this.skillMap.get(skillName); if (!skill) { throw new Error(`StubSkillLoader: unknown skill "${skillName}"`); @@ -58,7 +60,7 @@ class StubSkillLoader implements SkillLoaderInterface { async loadAll( skillNames: string[], - issue?: IssueCard, + issue?: IssueData, ): Promise { this.loadAllCalls.push({ skillNames, issue }); let results: ResolvedSkill[] = []; @@ -72,15 +74,39 @@ class StubSkillLoader implements SkillLoaderInterface { } } +class StubIssueRelationshipLoader implements IssueRelationshipLoader { + project: ProjectData | undefined; + knowledge: KnowledgeArticleData[]; + + constructor(opts?: { + project?: ProjectData | null; + knowledge?: KnowledgeArticleData[]; + }) { + this.project = + opts && 'project' in opts + ? (opts.project ?? undefined) + : { id: 'project-1', name: 'Sticky Notes' }; + this.knowledge = opts?.knowledge ?? []; + } + + async loadProject(_issue: IssueData): Promise { + return this.project; + } + + async loadKnowledge(_issue: IssueData): Promise { + return this.knowledge; + } +} + // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- -function makeProject(overrides?: Partial): ProjectCard { +function makeProject(overrides?: Partial): ProjectData { return { id: 'project-1', name: 'Sticky Notes', ...overrides }; } -function makeIssue(overrides?: Partial): IssueCard { +function makeIssue(overrides?: Partial): IssueData { return { id: 'issue-1', title: 'Implement StickyNote card', @@ -90,8 +116,8 @@ function makeIssue(overrides?: Partial): IssueCard { } function makeKnowledge( - overrides?: Partial, -): KnowledgeArticle { + overrides?: Partial, +): KnowledgeArticleData { return { id: 'ka-1', title: 'Boxel Card Basics', ...overrides }; } @@ -137,7 +163,6 @@ module('factory-context-builder > skill resolution', function () { issue, knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(resolver.calls.length, 1, 'resolve() called once'); @@ -173,7 +198,6 @@ module('factory-context-builder > skill resolution', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(loader.loadAllCalls.length, 1, 'loadAll() called once'); @@ -199,7 +223,6 @@ module('factory-context-builder > skill resolution', function () { issue, knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual( @@ -229,7 +252,6 @@ module('factory-context-builder > skill resolution', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.skills.length, 2, 'two skills in context'); @@ -266,7 +288,6 @@ module('factory-context-builder > skill budget', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.skills.length, 1, 'budget trimmed to one skill'); @@ -297,7 +318,6 @@ module('factory-context-builder > skill budget', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.skills.length, 2, 'all skills included'); @@ -318,7 +338,6 @@ module('factory-context-builder > tools excluded', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual( @@ -343,7 +362,6 @@ module('factory-context-builder > test results', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.testResults, undefined, 'no testResults'); @@ -370,7 +388,7 @@ module('factory-context-builder > test results', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', + testResults, }); @@ -393,7 +411,7 @@ module('factory-context-builder > test results', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', + testResults, }); @@ -422,7 +440,6 @@ module('factory-context-builder > core fields', function () { issue, knowledge, targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.project, project, 'project passed through'); @@ -439,14 +456,9 @@ module('factory-context-builder > core fields', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/my-realm/', - testRealmUrl: 'https://example.test/my-realm-test-artifacts/', }); assert.strictEqual(ctx.targetRealmUrl, 'https://example.test/my-realm/'); - assert.strictEqual( - ctx.testRealmUrl, - 'https://example.test/my-realm-test-artifacts/', - ); }); test('handles empty knowledge array', async function (assert) { @@ -458,9 +470,353 @@ module('factory-context-builder > core fields', function () { issue: makeIssue(), knowledge: [], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.deepEqual(ctx.knowledge, [], 'empty knowledge is fine'); }); }); + +// =========================================================================== +// Tests: buildForIssue (Phase 2) +// =========================================================================== + +function makeIssueConfig( + loaderOpts?: ConstructorParameters[0], + configOverrides?: Partial, +) { + let resolver = new StubSkillResolver(); + let loader = new StubSkillLoader([ + makeSkill('boxel-development'), + makeSkill('boxel-file-structure'), + makeSkill('ember-best-practices'), + ]); + let issueLoader = new StubIssueRelationshipLoader(loaderOpts); + + return { + config: { + skillResolver: resolver, + skillLoader: loader, + issueLoader, + ...configOverrides, + } as ContextBuilderConfig, + resolver, + loader, + issueLoader, + }; +} + +// --------------------------------------------------------------------------- +// Tests: buildForIssue — relationship traversal +// --------------------------------------------------------------------------- + +module('factory-context-builder > buildForIssue > relationships', function () { + test('loads project from issue.project relationship', async function (assert) { + let project = makeProject({ id: 'proj-99', name: 'Todo App' }); + let { config } = makeIssueConfig({ project }); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual(ctx.project.id, 'proj-99', 'project loaded from issue'); + assert.strictEqual(ctx.project.name, 'Todo App'); + }); + + test('loads knowledge from issue.relatedKnowledge', async function (assert) { + let knowledge = [ + makeKnowledge({ id: 'ka-1', title: 'Card Basics' }), + makeKnowledge({ id: 'ka-2', title: 'Styling Guide' }), + ]; + let { config } = makeIssueConfig({ knowledge }); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual(ctx.knowledge.length, 2, 'two knowledge articles'); + assert.strictEqual(ctx.knowledge[0].id, 'ka-1'); + assert.strictEqual(ctx.knowledge[1].id, 'ka-2'); + }); + + test('throws when issue has no linked project', async function (assert) { + let { config } = makeIssueConfig({ project: null }); + let builder = new ContextBuilder(config); + + try { + await builder.buildForIssue({ + issue: makeIssue({ id: 'orphan-issue' }), + targetRealmUrl: 'https://example.test/target/', + }); + assert.ok(false, 'should have thrown'); + } catch (error) { + assert.ok( + (error as Error).message.includes('orphan-issue'), + 'error mentions the issue id', + ); + assert.ok( + (error as Error).message.includes('no linked project'), + 'error explains the problem', + ); + } + }); +}); + +// --------------------------------------------------------------------------- +// Tests: buildForIssue — validation results +// --------------------------------------------------------------------------- + +module( + 'factory-context-builder > buildForIssue > validation results', + function () { + test('includes validation results when provided (2nd+ inner-loop iteration)', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + let validationResults: ValidationResults = { + passed: false, + steps: [ + { step: 'parse', passed: true, errors: [] }, + { + step: 'lint', + passed: false, + files: ['sticky-note.gts'], + errors: [ + { + file: 'sticky-note.gts', + message: "Expected ';' after statement", + }, + ], + }, + ], + }; + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + validationResults, + }); + + assert.deepEqual( + ctx.validationResults, + validationResults, + 'validation results included', + ); + assert.strictEqual( + ctx.validationResults?.steps.length, + 2, + 'two validation steps', + ); + assert.strictEqual( + ctx.validationResults?.steps[1].step, + 'lint', + 'lint step present', + ); + }); + + test('omits validation results on first inner-loop iteration (none provided)', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual( + ctx.validationResults, + undefined, + 'no validation results on first iteration', + ); + }); + + test('validation results include step name, file paths, error details', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + let validationResults: ValidationResults = { + passed: false, + steps: [ + { + step: 'evaluate', + passed: false, + files: ['sticky-note.gts'], + errors: [ + { + file: 'sticky-note.gts', + message: 'Cannot find module ./base-card', + stackTrace: 'at ModuleLoader.load (loader.ts:42)', + }, + ], + }, + { + step: 'test', + passed: false, + files: ['sticky-note.test.gts'], + errors: [ + { + file: 'sticky-note.test.gts', + message: 'Expected element to exist', + }, + ], + }, + ], + }; + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + validationResults, + }); + + let evalStep = ctx.validationResults?.steps[0]; + assert.strictEqual(evalStep?.step, 'evaluate'); + assert.deepEqual(evalStep?.files, ['sticky-note.gts']); + assert.strictEqual( + evalStep?.errors[0].stackTrace, + 'at ModuleLoader.load (loader.ts:42)', + ); + + let testStep = ctx.validationResults?.steps[1]; + assert.strictEqual(testStep?.step, 'test'); + assert.strictEqual( + testStep?.errors[0].message, + 'Expected element to exist', + ); + }); + }, +); + +// --------------------------------------------------------------------------- +// Tests: buildForIssue — skills +// --------------------------------------------------------------------------- + +module('factory-context-builder > buildForIssue > skills', function () { + test('skill selection works based on issue content', async function (assert) { + let { config, resolver } = makeIssueConfig(); + let builder = new ContextBuilder(config); + let issue = makeIssue({ + description: 'Create a .gts card definition with ember components', + }); + let project = makeProject({ id: 'project-1', name: 'Todo App' }); + // Pre-set the project so the resolver gets it + (config.issueLoader as StubIssueRelationshipLoader).project = project; + + await builder.buildForIssue({ + issue, + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual(resolver.calls.length, 1, 'resolver called once'); + assert.strictEqual( + resolver.calls[0].issue, + issue, + 'resolver received the issue', + ); + assert.strictEqual( + resolver.calls[0].project, + project, + 'resolver received the loaded project', + ); + }); + + test('token budget enforcement still works with buildForIssue', async function (assert) { + let resolver = new StubSkillResolver([ + 'boxel-development', + 'ember-best-practices', + ]); + let loader = new StubSkillLoader([ + makeSkill('boxel-development', 'A'.repeat(36)), // 9 tokens + makeSkill('ember-best-practices', 'B'.repeat(36)), // 9 tokens + ]); + let issueLoader = new StubIssueRelationshipLoader(); + let config: ContextBuilderConfig = { + skillResolver: resolver, + skillLoader: loader, + issueLoader, + maxSkillTokens: 10, + }; + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual(ctx.skills.length, 1, 'budget trimmed to one skill'); + assert.strictEqual( + ctx.skills[0].name, + 'boxel-development', + 'higher-priority skill kept', + ); + }); +}); + +// --------------------------------------------------------------------------- +// Tests: buildForIssue — bootstrap and context fields +// --------------------------------------------------------------------------- + +module( + 'factory-context-builder > buildForIssue > bootstrap and fields', + function () { + test('bootstrap issue context includes brief URL', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue({ issueType: 'bootstrap' }), + targetRealmUrl: 'https://example.test/target/', + briefUrl: 'https://example.test/briefs/sticky-notes', + }); + + assert.strictEqual( + ctx.briefUrl, + 'https://example.test/briefs/sticky-notes', + 'briefUrl included for bootstrap issue', + ); + }); + + test('omits briefUrl when not provided', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + + assert.strictEqual(ctx.briefUrl, undefined, 'no briefUrl'); + }); + + test('includes targetRealmUrl in context', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/my-realm/', + }); + + assert.strictEqual(ctx.targetRealmUrl, 'https://example.test/my-realm/'); + }); + + test('throws when issueLoader is not configured', async function (assert) { + let { config } = makeConfig(); // no issueLoader + let builder = new ContextBuilder(config); + + try { + await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + }); + assert.ok(false, 'should have thrown'); + } catch (error) { + assert.ok( + (error as Error).message.includes('issueLoader'), + 'error mentions issueLoader', + ); + } + }); + }, +); diff --git a/packages/software-factory/tests/factory-entrypoint.test.ts b/packages/software-factory/tests/factory-entrypoint.test.ts index c698dae40c..a7a5569627 100644 --- a/packages/software-factory/tests/factory-entrypoint.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.test.ts @@ -195,8 +195,6 @@ module('factory-entrypoint', function (hooks) { iterations: 1, toolCallLog: [], issueId: 'Issues/sticky-note-define-core', - testRealmUrl: - 'https://realms.example.test/hassan/personal-test-artifacts/', }), fetch: async (_input, init) => { assert.strictEqual( @@ -269,8 +267,6 @@ module('factory-entrypoint', function (hooks) { iterations: 1, toolCallLog: [], issueId: 'Issues/sticky-note-define-core', - testRealmUrl: - 'https://realms.example.test/app/hassan/personal-test-artifacts/', }), fetch: async () => new Response( diff --git a/packages/software-factory/tests/factory-implement.test.ts b/packages/software-factory/tests/factory-implement.test.ts index e6d3f27315..6ac26adccd 100644 --- a/packages/software-factory/tests/factory-implement.test.ts +++ b/packages/software-factory/tests/factory-implement.test.ts @@ -244,27 +244,6 @@ module('factory-implement', function () { ctx.targetRealmUrl, 'http://localhost:4201/test-user/my-realm/', ); - assert.strictEqual( - ctx.testRealmUrl, - 'http://localhost:4201/test-user/my-realm-test-artifacts/', - ); - }); - - test('derives test realm URL correctly', async function (assert) { - let agent = new MockLoopAgentForTest([{ status: 'done', toolCalls: [] }]); - - let config = makeConfig({ - agent, - targetRealmUrl: 'http://localhost:4201/hassan1/personal/', - }); - await runFactoryImplement(config); - - // The agent context should have the derived test realm URL - let ctx = agent.receivedContexts[0]; - assert.strictEqual( - ctx.testRealmUrl, - 'http://localhost:4201/hassan1/personal-test-artifacts/', - ); }); test('handles maxIterations configuration', async function (assert) { diff --git a/packages/software-factory/tests/factory-loop.test.ts b/packages/software-factory/tests/factory-loop.test.ts index b7dd0db533..f616a123d8 100644 --- a/packages/software-factory/tests/factory-loop.test.ts +++ b/packages/software-factory/tests/factory-loop.test.ts @@ -2,10 +2,10 @@ import { module, test } from 'qunit'; import type { AgentContext, - KnowledgeArticle, - ProjectCard, + KnowledgeArticleData, + ProjectData, TestResult, - IssueCard, + IssueData, } from '../src/factory-agent'; import type { FactoryTool, ToolCallEntry } from '../src/factory-tool-builder'; @@ -103,20 +103,18 @@ class MockFactoryAgent implements LoopAgent { class StubContextBuilder implements ContextBuilderLike { buildCalls: { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; testResults?: TestResult; }[] = []; async build(params: { - project: ProjectCard; - issue: IssueCard; - knowledge: KnowledgeArticle[]; + project: ProjectData; + issue: IssueData; + knowledge: KnowledgeArticleData[]; targetRealmUrl: string; - testRealmUrl: string; testResults?: TestResult; }): Promise { this.buildCalls.push(params); @@ -126,7 +124,6 @@ class StubContextBuilder implements ContextBuilderLike { knowledge: params.knowledge, skills: [], targetRealmUrl: params.targetRealmUrl, - testRealmUrl: params.testRealmUrl, testResults: params.testResults, }; } @@ -136,17 +133,17 @@ class StubContextBuilder implements ContextBuilderLike { // Fixtures // --------------------------------------------------------------------------- -function makeProject(overrides?: Partial): ProjectCard { +function makeProject(overrides?: Partial): ProjectData { return { id: 'project-1', name: 'Sticky Notes', ...overrides }; } -function makeIssue(overrides?: Partial): IssueCard { +function makeIssue(overrides?: Partial): IssueData { return { id: 'issue-1', title: 'Implement StickyNote card', ...overrides }; } function makeKnowledge( - overrides?: Partial, -): KnowledgeArticle { + overrides?: Partial, +): KnowledgeArticleData { return { id: 'ka-1', ...overrides }; } @@ -215,7 +212,6 @@ function makeLoopConfig( issue: makeIssue(), knowledge: [makeKnowledge()], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', ...overrides, }; } @@ -744,7 +740,6 @@ module('factory-loop > context threading', function () { agent, contextBuilder, targetRealmUrl: 'https://example.test/my-realm/', - testRealmUrl: 'https://example.test/my-realm-test-artifacts/', testRunner: makeTestRunner([ makeFailingTestResult(), makePassingTestResult(), @@ -761,14 +756,6 @@ module('factory-loop > context threading', function () { contextBuilder.buildCalls[1].targetRealmUrl, 'https://example.test/my-realm/', ); - assert.strictEqual( - contextBuilder.buildCalls[0].testRealmUrl, - 'https://example.test/my-realm-test-artifacts/', - ); - assert.strictEqual( - contextBuilder.buildCalls[1].testRealmUrl, - 'https://example.test/my-realm-test-artifacts/', - ); }); }); diff --git a/packages/software-factory/tests/factory-prompt-loader.test.ts b/packages/software-factory/tests/factory-prompt-loader.test.ts index 8d2adf558a..64379fcbb8 100644 --- a/packages/software-factory/tests/factory-prompt-loader.test.ts +++ b/packages/software-factory/tests/factory-prompt-loader.test.ts @@ -25,7 +25,6 @@ function makeMinimalContext(overrides?: Partial): AgentContext { skills: [], tools: [], targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', ...overrides, }; } @@ -192,7 +191,6 @@ module('factory-prompt-loader > FilePromptLoader', function () { let loader = new FilePromptLoader(); let result = loader.load('system', { targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/test/', skills: [], }); assert.ok( @@ -206,7 +204,6 @@ module('factory-prompt-loader > FilePromptLoader', function () { let loader = new FilePromptLoader(); let vars = { targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/test/', skills: [], }; let first = loader.load('system', vars); @@ -227,7 +224,6 @@ module('factory-prompt-loader > FilePromptLoader', function () { let loader = new FilePromptLoader(); let vars = { targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/test/', skills: [], }; let first = loader.load('system', vars); diff --git a/packages/software-factory/tests/factory-skill-loader.test.ts b/packages/software-factory/tests/factory-skill-loader.test.ts index 78d9191f0a..15fe96dafa 100644 --- a/packages/software-factory/tests/factory-skill-loader.test.ts +++ b/packages/software-factory/tests/factory-skill-loader.test.ts @@ -5,9 +5,9 @@ import { tmpdir } from 'node:os'; import { module, test } from 'qunit'; import type { - ProjectCard, + ProjectData, ResolvedSkill, - IssueCard, + IssueData, } from '../src/factory-agent'; import { DefaultSkillResolver, @@ -70,7 +70,7 @@ function writeSkill( } } -function makeIssue(overrides?: Partial): IssueCard { +function makeIssue(overrides?: Partial): IssueData { return { id: 'Issues/test-issue', title: 'Test issue', @@ -79,7 +79,7 @@ function makeIssue(overrides?: Partial): IssueCard { }; } -function makeProject(overrides?: Partial): ProjectCard { +function makeProject(overrides?: Partial): ProjectData { return { id: 'Projects/test-project', ...overrides, diff --git a/packages/software-factory/tests/factory-test-realm.spec.ts b/packages/software-factory/tests/factory-test-realm.spec.ts index 3aa71a1313..2186fd2552 100644 --- a/packages/software-factory/tests/factory-test-realm.spec.ts +++ b/packages/software-factory/tests/factory-test-realm.spec.ts @@ -211,7 +211,7 @@ test.describe('factory-test-realm e2e', () => { test('error path: unreachable realm returns error immediately', async () => { let options: TestRunRealmOptions = { - testRealmUrl: 'http://localhost:1/', + targetRealmUrl: 'http://localhost:1/', testResultsModuleUrl: 'http://localhost:1/software-factory/test-results', fetch: globalThis.fetch, }; diff --git a/packages/software-factory/tests/factory-test-realm.test.ts b/packages/software-factory/tests/factory-test-realm.test.ts index bf57bb0b9d..e311ccc395 100644 --- a/packages/software-factory/tests/factory-test-realm.test.ts +++ b/packages/software-factory/tests/factory-test-realm.test.ts @@ -19,7 +19,7 @@ import { pullRealmFiles } from '../src/realm-operations'; // --------------------------------------------------------------------------- const testRealmOptions = { - testRealmUrl: 'https://realms.example.test/user/personal-tests/', + targetRealmUrl: 'https://realms.example.test/user/personal-tests/', testResultsModuleUrl: 'https://realms.example.test/software-factory/test-results', realmServerUrl: 'https://realms.example.test/', diff --git a/packages/software-factory/tests/factory-tool-executor.integration.test.ts b/packages/software-factory/tests/factory-tool-executor.integration.test.ts index e2f7eeb6c4..4f5f65d269 100644 --- a/packages/software-factory/tests/factory-tool-executor.integration.test.ts +++ b/packages/software-factory/tests/factory-tool-executor.integration.test.ts @@ -90,7 +90,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: realmUrl, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-jwt-for-user', }); @@ -129,7 +128,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: realmUrl, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-jwt-for-user', }); @@ -173,7 +171,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: realmUrl, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-jwt-for-user', }); @@ -208,7 +205,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: realmUrl, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-jwt-for-user', }); @@ -254,7 +250,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-server-jwt-xyz', }); @@ -289,7 +284,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, authorization: 'Bearer realm-server-jwt-minted', }); @@ -341,7 +335,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, }); let result = await executor.execute('realm-server-session', { @@ -401,7 +394,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let sessionExecutor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: serverUrl, - testRealmUrl: `${origin}/user/target-tests/`, }); let sessionResult = await sessionExecutor.execute( @@ -420,7 +412,6 @@ module('factory-tool-executor integration > realm-api requests', function () { let createExecutor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: serverUrl, - testRealmUrl: `${origin}/user/target-tests/`, authorization: jwt, }); @@ -466,7 +457,6 @@ module('factory-tool-executor integration > safety constraints', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, }); try { @@ -501,7 +491,6 @@ module('factory-tool-executor integration > safety constraints', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, sourceRealmUrl: sourceUrl, }); @@ -536,7 +525,6 @@ module('factory-tool-executor integration > safety constraints', function () { let executor = new ToolExecutor(registry, { packageRoot: '/fake', targetRealmUrl: `${origin}/user/target/`, - testRealmUrl: `${origin}/user/target-tests/`, }); try { diff --git a/packages/software-factory/tests/factory-tool-executor.spec.ts b/packages/software-factory/tests/factory-tool-executor.spec.ts index 4f6093e4dc..2c94cbb03c 100644 --- a/packages/software-factory/tests/factory-tool-executor.spec.ts +++ b/packages/software-factory/tests/factory-tool-executor.spec.ts @@ -35,7 +35,6 @@ test('realm-read fetches .realm.json from the test realm', async ({ let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -54,7 +53,6 @@ test('realm-search returns results from the test realm', async ({ realm }) => { let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -84,7 +82,6 @@ test('realm-write creates a card and realm-read retrieves it', async ({ let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -130,7 +127,6 @@ test('realm-delete removes a card from the test realm', async ({ realm }) => { let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -181,7 +177,6 @@ test('unregistered tool is rejected without reaching the server', async ({ let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -208,7 +203,6 @@ async function buildToolsForRealm(realm: { let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -405,7 +399,6 @@ test.describe('realm-search with seeded fixture data', () => { let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -490,7 +483,6 @@ test.describe('realm-search on a private realm', () => { let ownerExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${realm.ownerBearerToken}`, }); @@ -534,7 +526,6 @@ test.describe('realm-search on a private realm', () => { let noAuthExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], // No authorization — simulates unauthenticated access }); @@ -553,7 +544,6 @@ test.describe('realm-search on a private realm', () => { let unauthorizedExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: `Bearer ${unauthorizedToken}`, }); @@ -635,7 +625,6 @@ test.describe('realm-create against a live realm server', () => { let sessionExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], }); @@ -656,7 +645,6 @@ test.describe('realm-create against a live realm server', () => { let createExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: realm.realmURL.href, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: serverJwt, }); @@ -686,7 +674,6 @@ test.describe('realm-create against a live realm server', () => { let verifyExecutor = new ToolExecutor(registry, { packageRoot: process.cwd(), targetRealmUrl: newRealmUrl, - testRealmUrl: realm.realmURL.href, allowedRealmPrefixes: [realm.realmURL.origin + '/'], authorization: newRealmToken, }); diff --git a/packages/software-factory/tests/factory-tool-executor.test.ts b/packages/software-factory/tests/factory-tool-executor.test.ts index 648f89c1b6..9ef8c786f6 100644 --- a/packages/software-factory/tests/factory-tool-executor.test.ts +++ b/packages/software-factory/tests/factory-tool-executor.test.ts @@ -24,7 +24,6 @@ function makeConfig( return { packageRoot: '/fake/software-factory', targetRealmUrl: 'https://realms.example.test/user/target/', - testRealmUrl: 'https://realms.example.test/user/target-tests/', ...overrides, }; } @@ -139,22 +138,6 @@ module('factory-tool-executor > source realm protection', function () { assert.strictEqual(result.exitCode, 0); }); - test('allows tool targeting test realm', async function (assert) { - let registry = new ToolRegistry(); - let config = makeConfig({ - sourceRealmUrl: 'https://realms.example.test/user/source/', - fetch: createMockFetch(200, { data: [] }), - }); - let executor = new ToolExecutor(registry, config); - - let result = await executor.execute('realm-read', { - 'realm-url': 'https://realms.example.test/user/target-tests/', - path: 'Test/spec.ts', - }); - - assert.strictEqual(result.exitCode, 0); - }); - test('rejects realm-api tool targeting unknown realm', async function (assert) { let registry = new ToolRegistry(); let executor = new ToolExecutor(