From d95251eaa95e8187b1efd61d4b4e4e1f9ebd0f34 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 30 Jul 2026 01:14:33 -0600 Subject: [PATCH] feat(profile): resolve remote skills for worktree agents --- docs/canonical-api.md | 26 +++++ src/improvement/improve.test.ts | 1 + src/mcp/worktree-harness.ts | 34 +++++- tests/mcp/worktree-harness.test.ts | 169 ++++++++++++++++++++++++----- 4 files changed, 201 insertions(+), 29 deletions(-) diff --git a/docs/canonical-api.md b/docs/canonical-api.md index aa4a7402..ab7a481f 100644 --- a/docs/canonical-api.md +++ b/docs/canonical-api.md @@ -44,6 +44,32 @@ It is separate from `runToolLoop` and `streamToolLoop` in `/tool-loop`, which ru An `AgentProfile` contains the agent's system prompt, skills, tools, MCP servers, subagents, hooks, permissions, memory, retrieval configuration, and model settings. Skills remain separate resources that the runtime can invoke; do not concatenate them into the system prompt. +Package-owned skills can remain in their source repository. +Reference the skill by an immutable commit and Runtime resolves it before a worktree worker starts: + +```ts +import { defineGitHubResource, type AgentProfile } from '@tangle-network/agent-interface' + +const tracesSkillsRef = process.env.TRACES_SKILLS_REF +if (!tracesSkillsRef) throw new Error('TRACES_SKILLS_REF must be an immutable commit') + +const profile: AgentProfile = { + name: 'trace-reviewer', + resources: { + failOnError: true, + skills: [ + defineGitHubResource('skills/inspect-agent-traces/SKILL.md', { + repository: 'tangle-network/traces', + ref: tracesSkillsRef, + name: 'inspect-agent-traces', + }), + ], + }, +} +``` + +The shared materializer normalizes the fetched markdown and mounts it in the selected agent's native skill directory. +The original profile remains unchanged, and the exact mounted bytes are covered by the materialization receipt. **You change an agent's behavior by changing its PROFILE: never by writing orchestration code around it.** The behaviors we keep hand-rolling are profile properties: - **Self-verification** is a profile lever, three ways, all configuration and zero glue code: (1) *steered*: the prompt says "run the tests, read failures, fix, repeat"; (2) *process-defined*: its instructions make verify-after-every-change its standing process; or (3) a **post-finish hook** that auto-runs the check and feeds failures back. The harness runs that loop. **You do not write a per-round judge, a `while(!done)`, or a bash hill-climb.** diff --git a/src/improvement/improve.test.ts b/src/improvement/improve.test.ts index 286583a7..32af9ce6 100644 --- a/src/improvement/improve.test.ts +++ b/src/improvement/improve.test.ts @@ -693,6 +693,7 @@ function createRepo(prefix: string): { stdio: ['ignore', 'pipe', 'pipe'], }) git(['init', '-q', '-b', 'main']) + git(['config', 'core.hooksPath', '/dev/null']) git(['config', 'user.email', 'improve@test.local']) git(['config', 'user.name', 'improve-test']) writeFileSync(join(repoRoot, 'module.txt'), 'baseline contents\n') diff --git a/src/mcp/worktree-harness.ts b/src/mcp/worktree-harness.ts index 062c8087..b63b08a2 100644 --- a/src/mcp/worktree-harness.ts +++ b/src/mcp/worktree-harness.ts @@ -26,6 +26,8 @@ import { applyWorkspacePlan, type HarnessId, materializeProfile, + type ResolveAgentProfileResourcesOptions, + resolveAgentProfileResources, type WorkspacePlan, type WorkspacePlanReceipt, } from '@tangle-network/agent-profile-materialize' @@ -172,6 +174,8 @@ export interface RunWorktreeHarnessOptions { checkOutputCap?: number /** Abort signal โ€” linked into the harness subprocess and the check commands. */ signal?: AbortSignal + /** Resource fetch options. GitHub-backed profile resources resolve before worker launch. */ + resourceResolution?: ResolveAgentProfileResourcesOptions /** Test seam โ€” inject a git runner so unit tests drive the worktree helpers without git. */ runGit?: GitRunner /** Test seam โ€” inject the harness runner so unit tests script a `LocalHarnessResult`. */ @@ -226,8 +230,13 @@ export async function runWorktreeHarness( try { assertSupportedWorktreeProfile(opts.profile, opts.harness) assertSafeProfileResourcePaths(opts.profile) - const resourceInstructions = resolveResourceInstructions(opts.profile) - const workspaceProfile = materializationOnlyProfile(opts.profile) + const profile = await resolveAgentProfileResources( + opts.profile, + resourceResolutionOptions(opts.resourceResolution, opts.signal), + ) + opts.signal?.throwIfAborted() + const resourceInstructions = resolveResourceInstructions(profile) + const workspaceProfile = materializationOnlyProfile(profile) const plan = materializeProfile(workspaceProfile, materializerHarness(opts.harness)) if (plan.unsupported.length > 0) { throw new Error( @@ -245,7 +254,7 @@ export async function runWorktreeHarness( // ยง1.5: the authored prompt + model reach the harness directly. Resource instructions use // the same explicit channel because reproducible Codex deliberately disables native project // instructions; the workspace projection therefore omits both prompt sources. - const invocationProfile = profileWithResourceInstructions(opts.profile, resourceInstructions) + const invocationProfile = profileWithResourceInstructions(profile, resourceInstructions) const { command, args } = harnessInvocation(opts.harness, invocationProfile, opts.taskPrompt, { // This helper created the candidate worktree above; autonomous Claude // edits are permitted only inside that isolated checkout. @@ -335,6 +344,25 @@ export async function runWorktreeHarness( } } +function resourceResolutionOptions( + options: ResolveAgentProfileResourcesOptions | undefined, + signal: AbortSignal | undefined, +): ResolveAgentProfileResourcesOptions | undefined { + if (!signal) return options + const fetchResource = options?.fetch ?? globalThis.fetch + return { + ...options, + fetch: (input, init) => { + signal.throwIfAborted() + const requestSignal = init?.signal + return fetchResource(input, { + ...init, + signal: requestSignal ? AbortSignal.any([signal, requestSignal]) : signal, + }) + }, + } +} + function resolveResourceInstructions(profile: AgentProfile): string | undefined { const instructions = profile.resources?.instructions if (instructions === undefined) return undefined diff --git a/tests/mcp/worktree-harness.test.ts b/tests/mcp/worktree-harness.test.ts index 48bba6bb..3622ebe5 100644 --- a/tests/mcp/worktree-harness.test.ts +++ b/tests/mcp/worktree-harness.test.ts @@ -32,6 +32,7 @@ function git(repoRoot: string, args: string[]): string { function initializeRepository(files: Record): string { const repoRoot = mkdtempSync(join(tmpdir(), 'agent-runtime-profile-worktree-')) git(repoRoot, ['init', '-q', '--initial-branch=main']) + git(repoRoot, ['config', 'core.hooksPath', '/dev/null']) git(repoRoot, ['config', 'user.email', 'runtime-test@example.invalid']) git(repoRoot, ['config', 'user.name', 'Runtime Test']) for (const [path, content] of Object.entries(files)) { @@ -475,32 +476,82 @@ describe('runWorktreeHarness profile materialization', () => { } }) - it('fails before worker launch and removes the real worktree for unsupported resources', async () => { + it('resolves and mounts a pinned Traces skill without changing the source profile or patch', async () => { const repoRoot = initializeRepository({ 'src/value.ts': 'export const value = 1\n' }) - const runId = 'unsupported-profile' - const runHarness = vi.fn() - try { - await expect( - runWorktreeHarness({ - repoRoot, - profile: { - resources: { - failOnError: false, - files: [ - { - path: '.agent-profile/remote.txt', - resource: { kind: 'github', repository: 'owner/repo', path: 'remote.txt' }, - }, - ], - }, + const runId = 'traces-skill' + const skillPath = '.codex/skills/inspect-agent-traces/SKILL.md' + const skillContent = [ + '---', + 'name: inspect-agent-traces', + 'description: Inspect an agent workflow with the Traces CLI.', + '---', + '', + '# Inspect Agent Traces', + '', + 'Run `traces analyze` and cite the exact sessions behind each finding.', + ].join('\n') + const profile: AgentProfile = { + resources: { + failOnError: true, + skills: [ + { + kind: 'github', + repository: 'tangle-network/traces', + path: 'skills/inspect-agent-traces/SKILL.md', + ref: '0123456789abcdef0123456789abcdef01234567', + name: 'inspect-agent-traces', }, - harness: 'codex', - taskPrompt: 'task', - runId, - runHarness, + ], + }, + } + const fetchResource = vi.fn( + async () => + new Response(skillContent, { + status: 200, + headers: { 'content-length': String(Buffer.byteLength(skillContent)) }, }), - ).rejects.toThrow(/profile cannot be materialized.*files/u) - expect(runHarness).not.toHaveBeenCalled() + ) as unknown as typeof fetch + try { + const run = await runWorktreeHarness({ + repoRoot, + profile, + harness: 'codex', + taskPrompt: 'task', + runId, + resourceResolution: { fetch: fetchResource }, + runHarness: async (options) => { + expect(readFileSync(join(options.cwd, skillPath), 'utf8')).toContain( + 'Run `traces analyze`', + ) + writeFileSync(join(options.cwd, 'src/value.ts'), 'export const value = 2\n') + writeFileSync(join(options.cwd, skillPath), 'worker changed mounted skill\n') + return successfulHarnessResult() + }, + }) + + try { + expect(fetchResource).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/tangle-network/traces/0123456789abcdef0123456789abcdef01234567/skills/inspect-agent-traces/SKILL.md', + expect.objectContaining({ signal: expect.any(AbortSignal) }), + ) + expect(profile.resources?.skills?.[0]).toEqual({ + kind: 'github', + repository: 'tangle-network/traces', + path: 'skills/inspect-agent-traces/SKILL.md', + ref: '0123456789abcdef0123456789abcdef01234567', + name: 'inspect-agent-traces', + }) + expect(run.result.profileMaterialization).toMatchObject({ + workspacePlanDigest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/u), + writtenPaths: [skillPath], + unsupported: [], + }) + expect(run.result.patch).toContain('+export const value = 2') + expect(run.result.patch).not.toContain(skillPath) + expect(run.result.patch).not.toContain('worker changed mounted skill') + } finally { + await run.cleanup() + } expect(existsSync(join(repoRoot, '.agent-worktrees', runId))).toBe(false) expect(git(repoRoot, ['branch', '--list', `delegate/${runId}`])).toBe('') } finally { @@ -508,9 +559,9 @@ describe('runWorktreeHarness profile materialization', () => { } }) - it('rejects unresolved resource instructions and removes the real worktree', async () => { + it('fails before worker launch and removes the real worktree when resource resolution fails', async () => { const repoRoot = initializeRepository({ 'src/value.ts': 'export const value = 1\n' }) - const runId = 'unresolved-resource-instructions' + const runId = 'failed-resource-resolution' const runHarness = vi.fn() try { await expect( @@ -529,8 +580,74 @@ describe('runWorktreeHarness profile materialization', () => { taskPrompt: 'task', runId, runHarness, + resourceResolution: { + fetch: (async () => + new Response('missing', { + status: 404, + statusText: 'Not Found', + })) as unknown as typeof fetch, + }, + }), + ).rejects.toThrow( + /Failed to fetch GitHub profile resource owner\/repo\/INSTRUCTIONS\.md@main: 404 Not Found/u, + ) + expect(runHarness).not.toHaveBeenCalled() + expect(existsSync(join(repoRoot, '.agent-worktrees', runId))).toBe(false) + expect(git(repoRoot, ['branch', '--list', `delegate/${runId}`])).toBe('') + } finally { + rmSync(repoRoot, { recursive: true, force: true }) + } + }) + + it('cancels resource resolution before worker launch and removes the real worktree', async () => { + const repoRoot = initializeRepository({ 'src/value.ts': 'export const value = 1\n' }) + const runId = 'cancelled-resource-resolution' + const controller = new AbortController() + const runHarness = vi.fn() + let markFetchStarted!: () => void + const fetchStarted = new Promise((resolve) => { + markFetchStarted = resolve + }) + const fetchResource = vi.fn( + async (_input: string | URL | Request, init?: RequestInit) => + new Promise((_resolve, reject) => { + markFetchStarted() + const signal = init?.signal + if (!signal) { + reject(new Error('resource fetch did not receive an abort signal')) + return + } + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) }), - ).rejects.toThrow(/resources\.instructions.*pre-resolution/u) + ) as unknown as typeof fetch + + try { + const running = runWorktreeHarness({ + repoRoot, + profile: { + resources: { + skills: [ + { + kind: 'github', + repository: 'tangle-network/traces', + path: 'skills/inspect-agent-traces/SKILL.md', + ref: '0123456789abcdef0123456789abcdef01234567', + }, + ], + }, + }, + harness: 'codex', + taskPrompt: 'task', + runId, + runHarness, + signal: controller.signal, + resourceResolution: { fetch: fetchResource }, + }) + + await fetchStarted + controller.abort(new Error('cancel remote skill resolution')) + + await expect(running).rejects.toThrow(/cancel remote skill resolution/u) expect(runHarness).not.toHaveBeenCalled() expect(existsSync(join(repoRoot, '.agent-worktrees', runId))).toBe(false) expect(git(repoRoot, ['branch', '--list', `delegate/${runId}`])).toBe('')