Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
1 change: 1 addition & 0 deletions src/improvement/improve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
34 changes: 31 additions & 3 deletions src/mcp/worktree-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
applyWorkspacePlan,
type HarnessId,
materializeProfile,
type ResolveAgentProfileResourcesOptions,
resolveAgentProfileResources,
type WorkspacePlan,
type WorkspacePlanReceipt,
} from '@tangle-network/agent-profile-materialize'
Expand Down Expand Up @@ -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`. */
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
169 changes: 143 additions & 26 deletions tests/mcp/worktree-harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function git(repoRoot: string, args: string[]): string {
function initializeRepository(files: Record<string, string>): 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)) {
Expand Down Expand Up @@ -475,42 +476,92 @@ 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 {
rmSync(repoRoot, { recursive: true, force: true })
}
})

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(
Expand All @@ -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<void>((resolve) => {
markFetchStarted = resolve
})
const fetchResource = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) =>
new Promise<Response>((_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('')
Expand Down