From b61a7be0f47d01291417df57e31918d428849499 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 15:29:03 -0400 Subject: [PATCH 1/7] CS-10676: Issue-aware ContextBuilder with buildForIssue() method Add buildForIssue() to ContextBuilder for the Phase 2 issue-driven loop. Instead of taking pre-loaded project/knowledge, it traverses issue relationships via an injected IssueRelationshipLoader to load project, knowledge articles, and resolved clarification answers automatically. New types: ValidationResults (parse/lint/evaluate/instantiate/test pipeline), ClarificationAnswer, IssueRelationshipLoader interface. AgentContext extended with validationResults, clarifications, briefUrl, and workspaceDir fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-agent-types.ts | 47 ++ .../scripts/lib/factory-context-builder.ts | 140 +++++ .../tests/factory-context-builder.test.ts | 512 ++++++++++++++++++ 3 files changed, 699 insertions(+) diff --git a/packages/software-factory/scripts/lib/factory-agent-types.ts b/packages/software-factory/scripts/lib/factory-agent-types.ts index 97395cbd33..ba8b9e4824 100644 --- a/packages/software-factory/scripts/lib/factory-agent-types.ts +++ b/packages/software-factory/scripts/lib/factory-agent-types.ts @@ -111,6 +111,45 @@ export interface TestResult { durationMs: number; } +// --------------------------------------------------------------------------- +// Validation types (Phase 2 — 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[]; +} + +/** A resolved clarification answer from a previously-blocked issue. */ +export interface ClarificationAnswer { + issueId: string; + question: string; + answer: string; +} + export interface ToolResult { tool: string; exitCode: number; @@ -134,6 +173,14 @@ export interface AgentContext { iteration?: number; targetRealmUrl: string; testRealmUrl: string; + /** Validation results from the prior inner-loop iteration (Phase 2). */ + validationResults?: ValidationResults; + /** Resolved clarification answers from previously-blocked issues (Phase 2). */ + clarifications?: ClarificationAnswer[]; + /** Brief URL for bootstrap issues (Phase 2). */ + briefUrl?: string; + /** Local workspace directory path (Phase 2). */ + workspaceDir?: string; } export interface AgentAction { diff --git a/packages/software-factory/scripts/lib/factory-context-builder.ts b/packages/software-factory/scripts/lib/factory-context-builder.ts index 86882d6e74..256af7819b 100644 --- a/packages/software-factory/scripts/lib/factory-context-builder.ts +++ b/packages/software-factory/scripts/lib/factory-context-builder.ts @@ -1,9 +1,11 @@ import type { AgentContext, + ClarificationAnswer, IssueCard, KnowledgeArticle, ProjectCard, TestResult, + ValidationResults, } from './factory-agent'; import type { ResolvedSkill } from './factory-agent'; @@ -14,6 +16,23 @@ import { type SkillResolver, } from './factory-skill-loader'; +// --------------------------------------------------------------------------- +// Issue relationship loader (Phase 2) +// --------------------------------------------------------------------------- + +/** + * Loads related cards from an issue's relationships. + * + * The Phase 2 `buildForIssue()` method uses this to traverse the issue's + * linksTo / linksToMany fields (project, relatedKnowledge, blockedBy) + * without coupling ContextBuilder to the realm I/O layer. + */ +export interface IssueRelationshipLoader { + loadProject(issue: IssueCard): Promise; + loadKnowledge(issue: IssueCard): Promise; + loadBlockedBy(issue: IssueCard): Promise; +} + // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- @@ -23,6 +42,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 +54,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; } /** @@ -89,4 +112,121 @@ export class ContextBuilder { return context; } + + /** + * Build agent context from the current issue (Phase 2 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 + * - resolved clarification answers from issue.blockedBy (done clarifications) + * + * Accepts optional validationResults from the prior inner-loop iteration + * so the agent can self-correct on failures. + */ + async buildForIssue(params: { + issue: IssueCard; + targetRealmUrl: string; + testRealmUrl: string; + workspaceDir?: string; + validationResults?: ValidationResults; + briefUrl?: string; + }): Promise { + if (!this.issueLoader) { + throw new Error( + 'buildForIssue() requires an issueLoader in ContextBuilderConfig', + ); + } + + let { issue, targetRealmUrl, testRealmUrl } = params; + + // Step 1: Traverse issue relationships + let [project, knowledge, blockedByIssues] = await Promise.all([ + this.issueLoader.loadProject(issue), + this.issueLoader.loadKnowledge(issue), + this.issueLoader.loadBlockedBy(issue), + ]); + + if (!project) { + throw new Error( + `Issue "${issue.id}" has no linked project — cannot build context`, + ); + } + + // Step 2: Extract clarification answers from resolved blockedBy issues + let clarifications = extractClarifications(blockedByIssues); + + // Step 3: Resolve and load skills + let skillNames = this.skillResolver.resolve(issue, project); + let skills: ResolvedSkill[] = await this.skillLoader.loadAll( + skillNames, + issue, + ); + + // Step 4: Enforce token budget if configured + skills = enforceSkillBudget(skills, this.maxSkillTokens); + + // Step 5: Assemble the context + let context: AgentContext = { + project, + issue, + knowledge, + skills, + targetRealmUrl, + testRealmUrl, + }; + + if (params.validationResults) { + context.validationResults = params.validationResults; + } + + if (clarifications.length > 0) { + context.clarifications = clarifications; + } + + if (params.briefUrl) { + context.briefUrl = params.briefUrl; + } + + if (params.workspaceDir) { + context.workspaceDir = params.workspaceDir; + } + + return context; + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Extract clarification answers from resolved blockedBy issues. + * + * A clarification issue is one with issueType === 'clarification' and + * status === 'done'. The question comes from the issue summary, and the + * answer from the issue description. + */ +function extractClarifications( + blockedByIssues: IssueCard[], +): ClarificationAnswer[] { + let clarifications: ClarificationAnswer[] = []; + + for (let blocked of blockedByIssues) { + if ( + blocked.issueType === 'clarification' && + blocked.status === 'done' && + typeof blocked.summary === 'string' && + typeof blocked.description === 'string' + ) { + clarifications.push({ + issueId: blocked.id, + question: blocked.summary, + answer: blocked.description, + }); + } + } + + return clarifications; } diff --git a/packages/software-factory/tests/factory-context-builder.test.ts b/packages/software-factory/tests/factory-context-builder.test.ts index b9f82de136..04b718e42c 100644 --- a/packages/software-factory/tests/factory-context-builder.test.ts +++ b/packages/software-factory/tests/factory-context-builder.test.ts @@ -5,12 +5,14 @@ import type { ProjectCard, ResolvedSkill, TestResult, + ValidationResults, IssueCard, } from '../scripts/lib/factory-agent'; import { ContextBuilder, type ContextBuilderConfig, + type IssueRelationshipLoader, } from '../scripts/lib/factory-context-builder'; import type { @@ -72,6 +74,37 @@ class StubSkillLoader implements SkillLoaderInterface { } } +class StubIssueRelationshipLoader implements IssueRelationshipLoader { + project: ProjectCard | undefined; + knowledge: KnowledgeArticle[]; + blockedBy: IssueCard[]; + + constructor(opts?: { + project?: ProjectCard | null; + knowledge?: KnowledgeArticle[]; + blockedBy?: IssueCard[]; + }) { + this.project = + opts && 'project' in opts + ? (opts.project ?? undefined) + : { id: 'project-1', name: 'Sticky Notes' }; + this.knowledge = opts?.knowledge ?? []; + this.blockedBy = opts?.blockedBy ?? []; + } + + async loadProject(_issue: IssueCard): Promise { + return this.project; + } + + async loadKnowledge(_issue: IssueCard): Promise { + return this.knowledge; + } + + async loadBlockedBy(_issue: IssueCard): Promise { + return this.blockedBy; + } +} + // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- @@ -464,3 +497,482 @@ module('factory-context-builder > core fields', function () { 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + 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('includes resolved clarification answers for previously-blocked issues', async function (assert) { + let blockedBy: IssueCard[] = [ + { + id: 'clarification-1', + issueType: 'clarification', + status: 'done', + summary: 'What color scheme should StickyNote use?', + description: 'Use yellow background with dark text.', + }, + { + id: 'clarification-2', + issueType: 'clarification', + status: 'done', + summary: 'Should StickyNote support markdown?', + description: 'No, plain text only for now.', + }, + ]; + let { config } = makeIssueConfig({ blockedBy }); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + assert.strictEqual(ctx.clarifications?.length, 2, 'two clarifications'); + assert.strictEqual(ctx.clarifications?.[0].issueId, 'clarification-1'); + assert.strictEqual( + ctx.clarifications?.[0].question, + 'What color scheme should StickyNote use?', + ); + assert.strictEqual( + ctx.clarifications?.[0].answer, + 'Use yellow background with dark text.', + ); + assert.strictEqual(ctx.clarifications?.[1].issueId, 'clarification-2'); + }); + + test('excludes non-clarification blockers and unresolved clarifications', async function (assert) { + let blockedBy: IssueCard[] = [ + // Regular implementation issue (not a clarification) — should be excluded + { + id: 'impl-1', + issueType: 'feature', + status: 'done', + summary: 'Implement base card', + description: 'Create the base card definition', + }, + // Clarification still blocked (not done) — should be excluded + { + id: 'clarification-pending', + issueType: 'clarification', + status: 'blocked', + summary: 'Pending question', + description: '', + }, + // Resolved clarification — should be included + { + id: 'clarification-done', + issueType: 'clarification', + status: 'done', + summary: 'What icon to use?', + description: 'Use the sticky-note icon from Phosphor.', + }, + ]; + let { config } = makeIssueConfig({ blockedBy }); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + assert.strictEqual( + ctx.clarifications?.length, + 1, + 'only one resolved clarification', + ); + assert.strictEqual(ctx.clarifications?.[0].issueId, 'clarification-done'); + }); + + test('omits clarifications field when none exist', async function (assert) { + let { config } = makeIssueConfig({ blockedBy: [] }); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + assert.strictEqual( + ctx.clarifications, + undefined, + 'no clarifications field when empty', + ); + }); + + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + 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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + + assert.strictEqual(ctx.briefUrl, undefined, 'no briefUrl'); + }); + + test('includes workspaceDir when provided', async function (assert) { + let { config } = makeIssueConfig(); + let builder = new ContextBuilder(config); + + let ctx = await builder.buildForIssue({ + issue: makeIssue(), + targetRealmUrl: 'https://example.test/target/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + workspaceDir: '/tmp/factory-workspace', + }); + + assert.strictEqual( + ctx.workspaceDir, + '/tmp/factory-workspace', + 'workspaceDir included', + ); + }); + + test('includes realm URLs 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/', + 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('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/', + testRealmUrl: 'https://example.test/target-test-artifacts/', + }); + assert.ok(false, 'should have thrown'); + } catch (error) { + assert.ok( + (error as Error).message.includes('issueLoader'), + 'error mentions issueLoader', + ); + } + }); + }, +); From 94ce4369d603ee45c860ce254d36fb502cff6790 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 15:32:45 -0400 Subject: [PATCH 2/7] Remove unnecessary "Phase 2" annotations from comments Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-agent-types.ts | 10 +++++----- .../scripts/lib/factory-context-builder.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/software-factory/scripts/lib/factory-agent-types.ts b/packages/software-factory/scripts/lib/factory-agent-types.ts index ba8b9e4824..316226591d 100644 --- a/packages/software-factory/scripts/lib/factory-agent-types.ts +++ b/packages/software-factory/scripts/lib/factory-agent-types.ts @@ -112,7 +112,7 @@ export interface TestResult { } // --------------------------------------------------------------------------- -// Validation types (Phase 2 — broader than TestResult) +// Validation types (broader than TestResult) // --------------------------------------------------------------------------- /** Steps in the post-iteration validation pipeline. */ @@ -173,13 +173,13 @@ export interface AgentContext { iteration?: number; targetRealmUrl: string; testRealmUrl: string; - /** Validation results from the prior inner-loop iteration (Phase 2). */ + /** Validation results from the prior inner-loop iteration. */ validationResults?: ValidationResults; - /** Resolved clarification answers from previously-blocked issues (Phase 2). */ + /** Resolved clarification answers from previously-blocked issues. */ clarifications?: ClarificationAnswer[]; - /** Brief URL for bootstrap issues (Phase 2). */ + /** Brief URL for bootstrap issues. */ briefUrl?: string; - /** Local workspace directory path (Phase 2). */ + /** Local workspace directory path. */ workspaceDir?: string; } diff --git a/packages/software-factory/scripts/lib/factory-context-builder.ts b/packages/software-factory/scripts/lib/factory-context-builder.ts index 256af7819b..2a6eebb399 100644 --- a/packages/software-factory/scripts/lib/factory-context-builder.ts +++ b/packages/software-factory/scripts/lib/factory-context-builder.ts @@ -17,13 +17,13 @@ import { } from './factory-skill-loader'; // --------------------------------------------------------------------------- -// Issue relationship loader (Phase 2) +// Issue relationship loader // --------------------------------------------------------------------------- /** * Loads related cards from an issue's relationships. * - * The Phase 2 `buildForIssue()` method uses this to traverse the issue's + * The `buildForIssue()` method uses this to traverse the issue's * linksTo / linksToMany fields (project, relatedKnowledge, blockedBy) * without coupling ContextBuilder to the realm I/O layer. */ @@ -114,7 +114,7 @@ export class ContextBuilder { } /** - * Build agent context from the current issue (Phase 2 issue-driven loop). + * 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: From 2862e56f57370f9829cf4b0335d069fe92994d5c Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 15:44:06 -0400 Subject: [PATCH 3/7] Address PR review: remove clarifications, testRealmUrl, workspaceDir from buildForIssue - Remove ClarificationAnswer type and clarifications field from AgentContext - Remove extractClarifications helper and loadBlockedBy from IssueRelationshipLoader - Remove testRealmUrl param from buildForIssue (only targetRealmUrl needed) - Remove workspaceDir from buildForIssue params and AgentContext - Remove all related tests; 25 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-agent-types.ts | 11 -- .../scripts/lib/factory-context-builder.ts | 65 +------- .../tests/factory-context-builder.test.ts | 144 +----------------- 3 files changed, 8 insertions(+), 212 deletions(-) diff --git a/packages/software-factory/scripts/lib/factory-agent-types.ts b/packages/software-factory/scripts/lib/factory-agent-types.ts index 316226591d..f626ae3d5f 100644 --- a/packages/software-factory/scripts/lib/factory-agent-types.ts +++ b/packages/software-factory/scripts/lib/factory-agent-types.ts @@ -143,13 +143,6 @@ export interface ValidationResults { steps: ValidationStepResult[]; } -/** A resolved clarification answer from a previously-blocked issue. */ -export interface ClarificationAnswer { - issueId: string; - question: string; - answer: string; -} - export interface ToolResult { tool: string; exitCode: number; @@ -175,12 +168,8 @@ export interface AgentContext { testRealmUrl: string; /** Validation results from the prior inner-loop iteration. */ validationResults?: ValidationResults; - /** Resolved clarification answers from previously-blocked issues. */ - clarifications?: ClarificationAnswer[]; /** Brief URL for bootstrap issues. */ briefUrl?: string; - /** Local workspace directory path. */ - workspaceDir?: string; } export interface AgentAction { diff --git a/packages/software-factory/scripts/lib/factory-context-builder.ts b/packages/software-factory/scripts/lib/factory-context-builder.ts index 2a6eebb399..3e9ca75d44 100644 --- a/packages/software-factory/scripts/lib/factory-context-builder.ts +++ b/packages/software-factory/scripts/lib/factory-context-builder.ts @@ -1,6 +1,5 @@ import type { AgentContext, - ClarificationAnswer, IssueCard, KnowledgeArticle, ProjectCard, @@ -24,13 +23,12 @@ import { * Loads related cards from an issue's relationships. * * The `buildForIssue()` method uses this to traverse the issue's - * linksTo / linksToMany fields (project, relatedKnowledge, blockedBy) + * linksTo / linksToMany fields (project, relatedKnowledge) * without coupling ContextBuilder to the realm I/O layer. */ export interface IssueRelationshipLoader { loadProject(issue: IssueCard): Promise; loadKnowledge(issue: IssueCard): Promise; - loadBlockedBy(issue: IssueCard): Promise; } // --------------------------------------------------------------------------- @@ -120,7 +118,6 @@ export class ContextBuilder { * traverses issue relationships to load them automatically: * - project from issue.project * - knowledge from issue.relatedKnowledge - * - resolved clarification answers from issue.blockedBy (done clarifications) * * Accepts optional validationResults from the prior inner-loop iteration * so the agent can self-correct on failures. @@ -128,8 +125,6 @@ export class ContextBuilder { async buildForIssue(params: { issue: IssueCard; targetRealmUrl: string; - testRealmUrl: string; - workspaceDir?: string; validationResults?: ValidationResults; briefUrl?: string; }): Promise { @@ -139,13 +134,12 @@ export class ContextBuilder { ); } - let { issue, targetRealmUrl, testRealmUrl } = params; + let { issue, targetRealmUrl } = params; // Step 1: Traverse issue relationships - let [project, knowledge, blockedByIssues] = await Promise.all([ + let [project, knowledge] = await Promise.all([ this.issueLoader.loadProject(issue), this.issueLoader.loadKnowledge(issue), - this.issueLoader.loadBlockedBy(issue), ]); if (!project) { @@ -154,79 +148,34 @@ export class ContextBuilder { ); } - // Step 2: Extract clarification answers from resolved blockedBy issues - let clarifications = extractClarifications(blockedByIssues); - - // Step 3: Resolve and load skills + // Step 2: Resolve and load skills let skillNames = this.skillResolver.resolve(issue, project); let skills: ResolvedSkill[] = await this.skillLoader.loadAll( skillNames, issue, ); - // Step 4: Enforce token budget if configured + // Step 3: Enforce token budget if configured skills = enforceSkillBudget(skills, this.maxSkillTokens); - // Step 5: Assemble the context + // Step 4: Assemble the context let context: AgentContext = { project, issue, knowledge, skills, targetRealmUrl, - testRealmUrl, + testRealmUrl: targetRealmUrl, }; if (params.validationResults) { context.validationResults = params.validationResults; } - if (clarifications.length > 0) { - context.clarifications = clarifications; - } - if (params.briefUrl) { context.briefUrl = params.briefUrl; } - if (params.workspaceDir) { - context.workspaceDir = params.workspaceDir; - } - return context; } } - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Extract clarification answers from resolved blockedBy issues. - * - * A clarification issue is one with issueType === 'clarification' and - * status === 'done'. The question comes from the issue summary, and the - * answer from the issue description. - */ -function extractClarifications( - blockedByIssues: IssueCard[], -): ClarificationAnswer[] { - let clarifications: ClarificationAnswer[] = []; - - for (let blocked of blockedByIssues) { - if ( - blocked.issueType === 'clarification' && - blocked.status === 'done' && - typeof blocked.summary === 'string' && - typeof blocked.description === 'string' - ) { - clarifications.push({ - issueId: blocked.id, - question: blocked.summary, - answer: blocked.description, - }); - } - } - - return clarifications; -} diff --git a/packages/software-factory/tests/factory-context-builder.test.ts b/packages/software-factory/tests/factory-context-builder.test.ts index 04b718e42c..92c40de596 100644 --- a/packages/software-factory/tests/factory-context-builder.test.ts +++ b/packages/software-factory/tests/factory-context-builder.test.ts @@ -77,19 +77,16 @@ class StubSkillLoader implements SkillLoaderInterface { class StubIssueRelationshipLoader implements IssueRelationshipLoader { project: ProjectCard | undefined; knowledge: KnowledgeArticle[]; - blockedBy: IssueCard[]; constructor(opts?: { project?: ProjectCard | null; knowledge?: KnowledgeArticle[]; - blockedBy?: IssueCard[]; }) { this.project = opts && 'project' in opts ? (opts.project ?? undefined) : { id: 'project-1', name: 'Sticky Notes' }; this.knowledge = opts?.knowledge ?? []; - this.blockedBy = opts?.blockedBy ?? []; } async loadProject(_issue: IssueCard): Promise { @@ -99,10 +96,6 @@ class StubIssueRelationshipLoader implements IssueRelationshipLoader { async loadKnowledge(_issue: IssueCard): Promise { return this.knowledge; } - - async loadBlockedBy(_issue: IssueCard): Promise { - return this.blockedBy; - } } // --------------------------------------------------------------------------- @@ -299,7 +292,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'); @@ -540,7 +532,6 @@ module('factory-context-builder > buildForIssue > relationships', function () { let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.project.id, 'proj-99', 'project loaded from issue'); @@ -558,7 +549,6 @@ module('factory-context-builder > buildForIssue > relationships', function () { let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.knowledge.length, 2, 'two knowledge articles'); @@ -566,106 +556,6 @@ module('factory-context-builder > buildForIssue > relationships', function () { assert.strictEqual(ctx.knowledge[1].id, 'ka-2'); }); - test('includes resolved clarification answers for previously-blocked issues', async function (assert) { - let blockedBy: IssueCard[] = [ - { - id: 'clarification-1', - issueType: 'clarification', - status: 'done', - summary: 'What color scheme should StickyNote use?', - description: 'Use yellow background with dark text.', - }, - { - id: 'clarification-2', - issueType: 'clarification', - status: 'done', - summary: 'Should StickyNote support markdown?', - description: 'No, plain text only for now.', - }, - ]; - let { config } = makeIssueConfig({ blockedBy }); - let builder = new ContextBuilder(config); - - let ctx = await builder.buildForIssue({ - issue: makeIssue(), - targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', - }); - - assert.strictEqual(ctx.clarifications?.length, 2, 'two clarifications'); - assert.strictEqual(ctx.clarifications?.[0].issueId, 'clarification-1'); - assert.strictEqual( - ctx.clarifications?.[0].question, - 'What color scheme should StickyNote use?', - ); - assert.strictEqual( - ctx.clarifications?.[0].answer, - 'Use yellow background with dark text.', - ); - assert.strictEqual(ctx.clarifications?.[1].issueId, 'clarification-2'); - }); - - test('excludes non-clarification blockers and unresolved clarifications', async function (assert) { - let blockedBy: IssueCard[] = [ - // Regular implementation issue (not a clarification) — should be excluded - { - id: 'impl-1', - issueType: 'feature', - status: 'done', - summary: 'Implement base card', - description: 'Create the base card definition', - }, - // Clarification still blocked (not done) — should be excluded - { - id: 'clarification-pending', - issueType: 'clarification', - status: 'blocked', - summary: 'Pending question', - description: '', - }, - // Resolved clarification — should be included - { - id: 'clarification-done', - issueType: 'clarification', - status: 'done', - summary: 'What icon to use?', - description: 'Use the sticky-note icon from Phosphor.', - }, - ]; - let { config } = makeIssueConfig({ blockedBy }); - let builder = new ContextBuilder(config); - - let ctx = await builder.buildForIssue({ - issue: makeIssue(), - targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', - }); - - assert.strictEqual( - ctx.clarifications?.length, - 1, - 'only one resolved clarification', - ); - assert.strictEqual(ctx.clarifications?.[0].issueId, 'clarification-done'); - }); - - test('omits clarifications field when none exist', async function (assert) { - let { config } = makeIssueConfig({ blockedBy: [] }); - let builder = new ContextBuilder(config); - - let ctx = await builder.buildForIssue({ - issue: makeIssue(), - targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', - }); - - assert.strictEqual( - ctx.clarifications, - undefined, - 'no clarifications field when empty', - ); - }); - test('throws when issue has no linked project', async function (assert) { let { config } = makeIssueConfig({ project: null }); let builder = new ContextBuilder(config); @@ -674,7 +564,6 @@ module('factory-context-builder > buildForIssue > relationships', function () { await builder.buildForIssue({ issue: makeIssue({ id: 'orphan-issue' }), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.ok(false, 'should have thrown'); } catch (error) { @@ -721,7 +610,6 @@ module( let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', validationResults, }); @@ -749,7 +637,6 @@ module( let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual( @@ -794,7 +681,6 @@ module( let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', validationResults, }); @@ -834,7 +720,6 @@ module('factory-context-builder > buildForIssue > skills', function () { await builder.buildForIssue({ issue, targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(resolver.calls.length, 1, 'resolver called once'); @@ -871,7 +756,6 @@ module('factory-context-builder > buildForIssue > skills', function () { let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.skills.length, 1, 'budget trimmed to one skill'); @@ -897,7 +781,6 @@ module( let ctx = await builder.buildForIssue({ issue: makeIssue({ issueType: 'bootstrap' }), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', briefUrl: 'https://example.test/briefs/sticky-notes', }); @@ -915,45 +798,21 @@ module( let ctx = await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.strictEqual(ctx.briefUrl, undefined, 'no briefUrl'); }); - test('includes workspaceDir when provided', async function (assert) { - let { config } = makeIssueConfig(); - let builder = new ContextBuilder(config); - - let ctx = await builder.buildForIssue({ - issue: makeIssue(), - targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', - workspaceDir: '/tmp/factory-workspace', - }); - - assert.strictEqual( - ctx.workspaceDir, - '/tmp/factory-workspace', - 'workspaceDir included', - ); - }); - - test('includes realm URLs in context', async function (assert) { + 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/', - 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('throws when issueLoader is not configured', async function (assert) { @@ -964,7 +823,6 @@ module( await builder.buildForIssue({ issue: makeIssue(), targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', }); assert.ok(false, 'should have thrown'); } catch (error) { From ce5772fb3b3b885324ff9d159e54c6a8cd4f24e0 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 16:00:24 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Rename=20IssueCard/ProjectCard/KnowledgeArt?= =?UTF-8?q?icle=20=E2=86=92=20IssueData/ProjectData/KnowledgeArticleData,?= =?UTF-8?q?=20remove=20testRealmUrl=20from=20AgentContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Card data types renamed to reflect they are flattened data, not card instances. testRealmUrl removed from AgentContext, ContextBuilder.build(), FactoryLoopConfig, and all consumers — test artifacts use the target realm. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-agent-tool-use.ts | 1 - .../scripts/lib/factory-agent-types.ts | 13 +++-- .../scripts/lib/factory-context-builder.ts | 23 ++++---- .../scripts/lib/factory-implement.ts | 19 ++++--- .../scripts/lib/factory-loop.ts | 21 ++++---- .../scripts/lib/factory-prompt-loader.ts | 1 - .../scripts/lib/factory-skill-loader.ts | 24 ++++----- .../smoke-tests/factory-agent-smoke.ts | 1 - .../smoke-tests/factory-context-smoke.ts | 19 +++---- .../scripts/smoke-tests/factory-loop-smoke.ts | 21 ++++---- .../smoke-tests/factory-prompt-smoke.ts | 1 - .../smoke-tests/factory-skill-smoke.ts | 8 +-- .../tests/factory-agent.integration.test.ts | 1 - .../tests/factory-agent.test.ts | 1 - .../tests/factory-context-builder.test.ts | 54 +++++++------------ .../tests/factory-implement.test.ts | 21 -------- .../tests/factory-loop.test.ts | 39 +++++--------- .../tests/factory-prompt-loader.test.ts | 4 -- .../tests/factory-skill-loader.test.ts | 8 +-- 19 files changed, 102 insertions(+), 178 deletions(-) diff --git a/packages/software-factory/scripts/lib/factory-agent-tool-use.ts b/packages/software-factory/scripts/lib/factory-agent-tool-use.ts index 627fcf2d9a..7f6f875fb9 100644 --- a/packages/software-factory/scripts/lib/factory-agent-tool-use.ts +++ b/packages/software-factory/scripts/lib/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/scripts/lib/factory-agent-types.ts b/packages/software-factory/scripts/lib/factory-agent-types.ts index f626ae3d5f..e9f5d14768 100644 --- a/packages/software-factory/scripts/lib/factory-agent-types.ts +++ b/packages/software-factory/scripts/lib/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; } @@ -151,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[]; @@ -165,7 +165,6 @@ 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. */ diff --git a/packages/software-factory/scripts/lib/factory-context-builder.ts b/packages/software-factory/scripts/lib/factory-context-builder.ts index 3e9ca75d44..9d2589d5c9 100644 --- a/packages/software-factory/scripts/lib/factory-context-builder.ts +++ b/packages/software-factory/scripts/lib/factory-context-builder.ts @@ -1,8 +1,8 @@ import type { AgentContext, - IssueCard, - KnowledgeArticle, - ProjectCard, + IssueData, + KnowledgeArticleData, + ProjectData, TestResult, ValidationResults, } from './factory-agent'; @@ -27,8 +27,8 @@ import { * without coupling ContextBuilder to the realm I/O layer. */ export interface IssueRelationshipLoader { - loadProject(issue: IssueCard): Promise; - loadKnowledge(issue: IssueCard): Promise; + loadProject(issue: IssueData): Promise; + loadKnowledge(issue: IssueData): Promise; } // --------------------------------------------------------------------------- @@ -71,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); @@ -100,7 +99,6 @@ export class ContextBuilder { knowledge, skills, targetRealmUrl, - testRealmUrl, }; // Include test results when iterating after a failed test run @@ -123,7 +121,7 @@ export class ContextBuilder { * so the agent can self-correct on failures. */ async buildForIssue(params: { - issue: IssueCard; + issue: IssueData; targetRealmUrl: string; validationResults?: ValidationResults; briefUrl?: string; @@ -165,7 +163,6 @@ export class ContextBuilder { knowledge, skills, targetRealmUrl, - testRealmUrl: targetRealmUrl, }; if (params.validationResults) { diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 343f5eae3e..9b2d81d231 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -16,9 +16,9 @@ import { resolve } from 'node:path'; import type { - IssueCard, - KnowledgeArticle, - ProjectCard, + IssueData, + KnowledgeArticleData, + ProjectData, TestResult, } from './factory-agent'; import { @@ -240,7 +240,6 @@ export async function runFactoryImplement( issue, knowledge, targetRealmUrl, - testRealmUrl, maxIterations: config.maxIterations, }); @@ -379,9 +378,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( @@ -398,7 +397,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); @@ -459,7 +458,7 @@ interface TestRunnerConfig { */ function buildTestRunner( targetRealmUrl: string, - issue: IssueCard, + issue: IssueData, toolCallLog: ToolCallEntry[], runConfig: TestRunnerConfig, ): TestRunner { @@ -731,7 +730,7 @@ const BASE_CARD_TYPES: { module: string; name: string }[] = [ /** * Fetch JSON schemas for card types the factory uses. Includes both - * DarkFactory types (Project, Issue, KnowledgeArticle) from the target + * DarkFactory types (Project, Issue, KnowledgeArticleData) from the target * realm and base types (Spec) from the base realm. Returns a Map * suitable for passing to ToolBuilderConfig.cardTypeSchemas. */ diff --git a/packages/software-factory/scripts/lib/factory-loop.ts b/packages/software-factory/scripts/lib/factory-loop.ts index 8451493bcc..682b3c2ac8 100644 --- a/packages/software-factory/scripts/lib/factory-loop.ts +++ b/packages/software-factory/scripts/lib/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/scripts/lib/factory-prompt-loader.ts b/packages/software-factory/scripts/lib/factory-prompt-loader.ts index 454f6a7829..3ac6d61d1e 100644 --- a/packages/software-factory/scripts/lib/factory-prompt-loader.ts +++ b/packages/software-factory/scripts/lib/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/scripts/lib/factory-skill-loader.ts b/packages/software-factory/scripts/lib/factory-skill-loader.ts index 41884999c6..d1bed6cd91 100644 --- a/packages/software-factory/scripts/lib/factory-skill-loader.ts +++ b/packages/software-factory/scripts/lib/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/scripts/smoke-tests/factory-agent-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts index 009aeb4042..d053589f87 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts @@ -110,7 +110,6 @@ async function main(): Promise { skills: [], tools: [], targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-tests/', }; console.log('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 08760c2aac..92f6fd9ee9 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts @@ -12,9 +12,9 @@ import { parseArgs } from 'node:util'; import type { - KnowledgeArticle, - ProjectCard, - IssueCard, + KnowledgeArticleData, + ProjectData, + IssueData, } from '../lib/factory-agent'; import { ContextBuilder } from '../lib/factory-context-builder'; import { @@ -44,12 +44,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', @@ -62,7 +62,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: { @@ -146,7 +146,6 @@ async function main(): Promise { issue, knowledge: SAMPLE_KNOWLEDGE, targetRealmUrl: 'https://example.test/user/target/', - testRealmUrl: 'https://example.test/user/target-test-artifacts/', }); console.log(' First pass (no test results):'); @@ -166,10 +165,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); console.log(` Skill breakdown (~${totalTokens} total tokens):`); @@ -189,7 +184,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, @@ -248,7 +242,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 efc547877a..709a1a6869 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts @@ -13,10 +13,10 @@ import { parseArgs } from 'node:util'; import type { AgentContext, - KnowledgeArticle, - ProjectCard, + KnowledgeArticleData, + ProjectData, TestResult, - IssueCard, + IssueData, } from '../lib/factory-agent'; import type { FactoryTool, ToolCallEntry } from '../lib/factory-tool-builder'; @@ -109,11 +109,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 { @@ -122,7 +121,6 @@ class StubContextBuilder implements ContextBuilderLike { knowledge: params.knowledge, skills: [], targetRealmUrl: params.targetRealmUrl, - testRealmUrl: params.testRealmUrl, testResults: params.testResults, }; } @@ -132,18 +130,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' }, ]; @@ -189,7 +187,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 cfef5e388f..563def4fea 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts @@ -104,7 +104,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 cfd5f5c283..adc7eb848b 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -17,9 +17,9 @@ import { enforceSkillBudget, estimateTokens, } from '../lib/factory-skill-loader'; -import type { ProjectCard, IssueCard } from '../lib/factory-agent'; +import type { ProjectData, IssueData } from '../lib/factory-agent'; -const SAMPLE_ISSUES: { label: string; issue: IssueCard }[] = [ +const SAMPLE_ISSUES: { label: string; issue: IssueData }[] = [ { label: 'Generic card work (base case)', issue: { @@ -88,7 +88,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' }; console.log('=== Skill Loader & Resolver Smoke Test ===\n'); @@ -101,7 +101,7 @@ async function main(): Promise { id: 'Issues/custom', title: customIssueText, description: customIssueText, - } as IssueCard, + } as IssueData, }, ] : SAMPLE_ISSUES; diff --git a/packages/software-factory/tests/factory-agent.integration.test.ts b/packages/software-factory/tests/factory-agent.integration.test.ts index f9713841db..dae3e5fdb3 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 b254114e3d..e9ba6ad5aa 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 92c40de596..c123974664 100644 --- a/packages/software-factory/tests/factory-context-builder.test.ts +++ b/packages/software-factory/tests/factory-context-builder.test.ts @@ -1,12 +1,12 @@ import { module, test } from 'qunit'; import type { - KnowledgeArticle, - ProjectCard, + KnowledgeArticleData, + ProjectData, ResolvedSkill, TestResult, ValidationResults, - IssueCard, + IssueData, } from '../scripts/lib/factory-agent'; import { @@ -28,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; } @@ -44,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}"`); @@ -60,7 +60,7 @@ class StubSkillLoader implements SkillLoaderInterface { async loadAll( skillNames: string[], - issue?: IssueCard, + issue?: IssueData, ): Promise { this.loadAllCalls.push({ skillNames, issue }); let results: ResolvedSkill[] = []; @@ -75,12 +75,12 @@ class StubSkillLoader implements SkillLoaderInterface { } class StubIssueRelationshipLoader implements IssueRelationshipLoader { - project: ProjectCard | undefined; - knowledge: KnowledgeArticle[]; + project: ProjectData | undefined; + knowledge: KnowledgeArticleData[]; constructor(opts?: { - project?: ProjectCard | null; - knowledge?: KnowledgeArticle[]; + project?: ProjectData | null; + knowledge?: KnowledgeArticleData[]; }) { this.project = opts && 'project' in opts @@ -89,11 +89,11 @@ class StubIssueRelationshipLoader implements IssueRelationshipLoader { this.knowledge = opts?.knowledge ?? []; } - async loadProject(_issue: IssueCard): Promise { + async loadProject(_issue: IssueData): Promise { return this.project; } - async loadKnowledge(_issue: IssueCard): Promise { + async loadKnowledge(_issue: IssueData): Promise { return this.knowledge; } } @@ -102,11 +102,11 @@ class StubIssueRelationshipLoader implements IssueRelationshipLoader { // 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', @@ -116,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 }; } @@ -163,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'); @@ -199,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'); @@ -225,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( @@ -255,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'); @@ -322,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'); @@ -343,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( @@ -368,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'); @@ -395,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, }); @@ -418,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, }); @@ -447,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'); @@ -464,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) { @@ -483,7 +470,6 @@ 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'); diff --git a/packages/software-factory/tests/factory-implement.test.ts b/packages/software-factory/tests/factory-implement.test.ts index 86b6834cc6..b035adf166 100644 --- a/packages/software-factory/tests/factory-implement.test.ts +++ b/packages/software-factory/tests/factory-implement.test.ts @@ -247,27 +247,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 6ea55c7c27..a53eed2208 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 '../scripts/lib/factory-agent'; import type { @@ -106,20 +106,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); @@ -129,7 +127,6 @@ class StubContextBuilder implements ContextBuilderLike { knowledge: params.knowledge, skills: [], targetRealmUrl: params.targetRealmUrl, - testRealmUrl: params.testRealmUrl, testResults: params.testResults, }; } @@ -139,17 +136,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 }; } @@ -218,7 +215,6 @@ function makeLoopConfig( issue: makeIssue(), knowledge: [makeKnowledge()], targetRealmUrl: 'https://example.test/target/', - testRealmUrl: 'https://example.test/target-test-artifacts/', ...overrides, }; } @@ -747,7 +743,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(), @@ -764,14 +759,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 e875dc6699..2464e8aab2 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 11807bb4b3..02a63192aa 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 '../scripts/lib/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, From 8d95ca43bd685aba5c93eaf4e8f98d8584b5981d Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 16:27:24 -0400 Subject: [PATCH 5/7] Remove {{testRealmUrl}} from system.md template, fix KnowledgeArticleData comment Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/prompts/system.md | 1 - packages/software-factory/scripts/lib/factory-implement.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 9b2d81d231..88ae31a3a3 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -730,7 +730,7 @@ const BASE_CARD_TYPES: { module: string; name: string }[] = [ /** * Fetch JSON schemas for card types the factory uses. Includes both - * DarkFactory types (Project, Issue, KnowledgeArticleData) from the target + * DarkFactory types (Project, Issue, KnowledgeArticle) from the target * realm and base types (Spec) from the base realm. Returns a Map * suitable for passing to ToolBuilderConfig.cardTypeSchemas. */ From 1683336ac014919bb9f90cab2765884e6bf01ac0 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 16:34:18 -0400 Subject: [PATCH 6/7] Remove testRealmUrl from tool executor, test-run infra, and all remaining consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes testRealmUrl removal across the entire codebase: - ToolExecutorConfig, test-run-types, test-run-execution, test-run-cards - factory-implement.ts (derived URL and loop config) - factory-entrypoint.test.ts (ImplementResult stubs) - All tool executor tests, integration tests, and smoke tests - Renamed testRealmUrl → targetRealmUrl in TestRunRealmOptions Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-implement.ts | 5 ----- .../scripts/lib/factory-tool-executor.ts | 10 +--------- .../software-factory/scripts/lib/test-run-cards.ts | 10 +++++++--- .../scripts/lib/test-run-execution.ts | 14 +++++++------- .../software-factory/scripts/lib/test-run-types.ts | 2 +- .../scripts/smoke-tests/factory-tools-smoke.ts | 3 --- .../tests/factory-entrypoint.test.ts | 4 ---- .../tests/factory-test-realm.spec.ts | 2 +- .../tests/factory-test-realm.test.ts | 2 +- .../factory-tool-executor.integration.test.ts | 12 ------------ .../tests/factory-tool-executor.spec.ts | 13 ------------- .../tests/factory-tool-executor.test.ts | 1 - 12 files changed, 18 insertions(+), 60 deletions(-) diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 88ae31a3a3..23f74290c5 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -133,10 +133,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); @@ -156,7 +152,6 @@ export async function runFactoryImplement( let toolExecutor = new ToolExecutor(toolRegistry, { packageRoot: PACKAGE_ROOT, targetRealmUrl, - testRealmUrl, fetch: fetchImpl, authorization: config.authorization, }); diff --git a/packages/software-factory/scripts/lib/factory-tool-executor.ts b/packages/software-factory/scripts/lib/factory-tool-executor.ts index fc7f66beba..daab6729c1 100644 --- a/packages/software-factory/scripts/lib/factory-tool-executor.ts +++ b/packages/software-factory/scripts/lib/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/scripts/lib/test-run-cards.ts b/packages/software-factory/scripts/lib/test-run-cards.ts index 151329a339..7ad735367b 100644 --- a/packages/software-factory/scripts/lib/test-run-cards.ts +++ b/packages/software-factory/scripts/lib/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/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index 4a1b68bda0..fa10be8be1 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -32,7 +32,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, @@ -79,10 +79,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' }, @@ -120,8 +120,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 { @@ -135,7 +135,7 @@ async function getNextSequenceNumber( options: TestRunRealmOptions, ): Promise { let result = await searchRealm( - options.testRealmUrl, + options.targetRealmUrl, { filter: { on: { module: options.testResultsModuleUrl, name: 'TestRun' }, @@ -402,7 +402,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/scripts/lib/test-run-types.ts b/packages/software-factory/scripts/lib/test-run-types.ts index 0397158e3c..25149ffeec 100644 --- a/packages/software-factory/scripts/lib/test-run-types.ts +++ b/packages/software-factory/scripts/lib/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/scripts/smoke-tests/factory-tools-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts index 3ea188dbf7..70f07d61db 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts @@ -131,7 +131,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-'], }); @@ -178,7 +177,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); @@ -245,7 +243,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/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-test-realm.spec.ts b/packages/software-factory/tests/factory-test-realm.spec.ts index e8bef031e6..9e092d4377 100644 --- a/packages/software-factory/tests/factory-test-realm.spec.ts +++ b/packages/software-factory/tests/factory-test-realm.spec.ts @@ -215,7 +215,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 2c3338121c..4d3157a06c 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 '../scripts/lib/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 5dd2b600b1..6c8b958772 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 fed21fb9f5..e31a1a27dd 100644 --- a/packages/software-factory/tests/factory-tool-executor.spec.ts +++ b/packages/software-factory/tests/factory-tool-executor.spec.ts @@ -38,7 +38,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}`, }); @@ -57,7 +56,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}`, }); @@ -87,7 +85,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}`, }); @@ -133,7 +130,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}`, }); @@ -184,7 +180,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}`, }); @@ -211,7 +206,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}`, }); @@ -408,7 +402,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}`, }); @@ -493,7 +486,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}`, }); @@ -537,7 +529,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 }); @@ -556,7 +547,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}`, }); @@ -638,7 +628,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 + '/'], }); @@ -659,7 +648,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, }); @@ -689,7 +677,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 60c6ea048c..fa32cc64b6 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, }; } From ff8ecc6d1f1cb25e8bb8ef70b3eccbcebb983b9d Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 16:36:43 -0400 Subject: [PATCH 7/7] Remove test realm test case and fix tool executor allowed list The "allows tool targeting test realm" test is no longer valid since testRealmUrl was removed from ToolExecutorConfig. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/factory-tool-executor.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/software-factory/tests/factory-tool-executor.test.ts b/packages/software-factory/tests/factory-tool-executor.test.ts index fa32cc64b6..c4af568a1b 100644 --- a/packages/software-factory/tests/factory-tool-executor.test.ts +++ b/packages/software-factory/tests/factory-tool-executor.test.ts @@ -138,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(