Skip to content

Commit 362c51f

Browse files
gewenyu99claude
andcommitted
fix(agent): drop EU MCP endpoint, resolve region from the bearer token
The server reads the user's region from the bearer token, so the EU subdomain (a Claude Code OAuth workaround) is no longer needed. Removes the host-parsing branch in both the orchestrator bootstrap and the linear MCP runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 64cebf4 commit 362c51f

4 files changed

Lines changed: 605 additions & 37 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import {
5+
agentRunTools,
6+
assembleTaskPrompt,
7+
buildRegistry,
8+
parseAgentPrompt,
9+
resolveTask,
10+
taskModel,
11+
type AgentPrompt,
12+
type AgentRegistry,
13+
type OrchestratorPromptContext,
14+
} from '../agent-prompt-loader';
15+
import { QueueStore } from '../../programs/orchestrator/queue';
16+
17+
function tmpDir(): string {
18+
return fs.mkdtempSync(path.join(os.tmpdir(), 'agent-loader-test-'));
19+
}
20+
21+
function registryOf(prompts: AgentPrompt[]): AgentRegistry {
22+
return buildRegistry(
23+
prompts.map((p) => ({ ...p, flow: 'test-flow' })),
24+
'test-flow',
25+
);
26+
}
27+
28+
describe('parseAgentPrompt', () => {
29+
const sample = `---
30+
type: instrument-events
31+
model: claude-sonnet-4-6 # cheapest model that succeeds
32+
skills: [instrument-events]
33+
allowedTools: [Read, Edit, Grep, Glob, Bash]
34+
disallowedTools: [enqueue_task]
35+
dependsOn: [init]
36+
---
37+
38+
## Goal
39+
Add at least one capture call.
40+
`;
41+
42+
it('parses frontmatter scalars and inline arrays', () => {
43+
const p = parseAgentPrompt(sample, 'fallback');
44+
expect(p.type).toBe('instrument-events');
45+
expect(p.model).toBe('claude-sonnet-4-6');
46+
expect(p.skills).toEqual(['instrument-events']);
47+
expect(p.allowedTools).toEqual(['Read', 'Edit', 'Grep', 'Glob', 'Bash']);
48+
expect(p.disallowedTools).toEqual(['enqueue_task']);
49+
expect(p.dependsOn).toEqual(['init']);
50+
});
51+
52+
it('strips inline comments and keeps the body', () => {
53+
const p = parseAgentPrompt(sample, 'fallback');
54+
expect(p.model).not.toContain('#');
55+
expect(p.body).toContain('## Goal');
56+
expect(p.body).not.toContain('---');
57+
});
58+
59+
it('falls back to the menu id when type is omitted', () => {
60+
const p = parseAgentPrompt('---\nmodel: x\n---\nbody', 'install');
61+
expect(p.type).toBe('install');
62+
});
63+
64+
it('parses the flow from frontmatter', () => {
65+
const p = parseAgentPrompt('---\nflow: audit\n---\nx', 'fix-events');
66+
expect(p.flow).toBe('audit');
67+
});
68+
69+
it('marks the seed from frontmatter; everything else is a task', () => {
70+
expect(parseAgentPrompt('---\nseed: true\n---\nplan', 'planner').seed).toBe(
71+
true,
72+
);
73+
expect(parseAgentPrompt('---\nmodel: x\n---\nbody', 'install').seed).toBe(
74+
false,
75+
);
76+
});
77+
78+
it('defaults missing array fields to empty and model to undefined', () => {
79+
const p = parseAgentPrompt('no frontmatter at all', 'stub');
80+
expect(p.model).toBeUndefined();
81+
expect(p.skills).toEqual([]);
82+
expect(p.dependsOn).toEqual([]);
83+
expect(p.body).toBe('no frontmatter at all');
84+
});
85+
});
86+
87+
describe('agentRunTools', () => {
88+
it('MCP-qualifies orchestrator tools and passes native tools through', () => {
89+
const p = parseAgentPrompt(
90+
'---\nallowedTools: [Read, read_handoffs]\ndisallowedTools: [enqueue_task, complete_task, Bash]\n---\nx',
91+
't',
92+
);
93+
const { allowedTools, disallowedTools } = agentRunTools(p);
94+
expect(allowedTools).toEqual([
95+
'Read',
96+
'mcp__posthog-wizard__read_handoffs',
97+
]);
98+
expect(disallowedTools).toEqual([
99+
'mcp__posthog-wizard__enqueue_task',
100+
'mcp__posthog-wizard__complete_task',
101+
'Bash',
102+
]);
103+
});
104+
});
105+
106+
describe('buildRegistry', () => {
107+
const prompt = (over: Partial<AgentPrompt>): AgentPrompt => ({
108+
type: 'x',
109+
seed: false,
110+
skills: [],
111+
allowedTools: [],
112+
disallowedTools: [],
113+
dependsOn: [],
114+
body: 'b',
115+
...over,
116+
});
117+
118+
it('scopes to one flow and keeps the seed out of the task types', () => {
119+
const registry = buildRegistry(
120+
[
121+
prompt({ type: 'plan-audit', flow: 'audit', seed: true }),
122+
prompt({ type: 'fix-events', flow: 'audit' }),
123+
prompt({ type: 'install', flow: 'posthog-integration' }),
124+
prompt({ type: 'example' }),
125+
],
126+
'audit',
127+
);
128+
expect(registry.types).toEqual(['fix-events']);
129+
expect(registry.seed?.type).toBe('plan-audit');
130+
expect(registry.get('install')).toBeUndefined();
131+
// A flowless prompt (e.g. the documentation example) joins no registry.
132+
expect(registry.get('example')).toBeUndefined();
133+
});
134+
135+
it('drops harness-excluded types; unrestricted runs keep them', () => {
136+
const prompts = [
137+
prompt({ type: 'plan', flow: 'f', seed: true }),
138+
prompt({ type: 'build', flow: 'f' }),
139+
prompt({ type: 'dashboard', flow: 'f' }),
140+
];
141+
expect(
142+
buildRegistry(prompts, 'f', { exclude: ['dashboard'] }).types,
143+
).toEqual(['build']);
144+
expect(buildRegistry(prompts, 'f').types).toEqual(['build', 'dashboard']);
145+
});
146+
});
147+
148+
describe('resolveTask', () => {
149+
let dir: string;
150+
let store: QueueStore;
151+
152+
beforeEach(() => {
153+
dir = tmpDir();
154+
store = new QueueStore(dir, 'run-1');
155+
});
156+
157+
afterEach(() => {
158+
fs.rmSync(dir, { recursive: true, force: true });
159+
});
160+
161+
const prompt: AgentPrompt = {
162+
type: 'capture',
163+
seed: false,
164+
model: 'claude-haiku-4-5-20251001',
165+
skills: ['instrument-events'],
166+
allowedTools: ['Read', 'Edit'],
167+
disallowedTools: ['enqueue_task'],
168+
dependsOn: ['plan-capture'],
169+
body: '## Goal\nInstrument the planned events.',
170+
};
171+
172+
it('throws when no prompt is registered for the type', () => {
173+
const registry = registryOf([]);
174+
const task = { type: 'capture', dependsOn: [] } as never;
175+
expect(() => resolveTask(registry, task, store)).toThrow(/capture/);
176+
});
177+
178+
it('resolves model, tools, and skills from the prompt', () => {
179+
const registry = registryOf([prompt]);
180+
const task = store.enqueue({ type: 'capture' });
181+
const resolved = resolveTask(registry, task, store);
182+
expect(resolved.model).toBe('claude-haiku-4-5-20251001');
183+
expect(resolved.skills).toEqual(['instrument-events']);
184+
expect(resolved.disallowedTools).toEqual([
185+
'mcp__posthog-wizard__enqueue_task',
186+
]);
187+
});
188+
189+
it('prefers the enqueue model override over the prompt model', () => {
190+
const registry = registryOf([prompt]);
191+
const task = store.enqueue({ type: 'capture', model: 'override-x' });
192+
expect(resolveTask(registry, task, store).model).toBe('override-x');
193+
});
194+
195+
it("appends upstream dependencies' handoffs as context", () => {
196+
const registry = registryOf([prompt]);
197+
const dep = store.enqueue({ type: 'plan-capture' });
198+
store.complete(dep.id, {
199+
goals: 'decide events',
200+
did: 'picked signup and purchase',
201+
forNextAgent: 'instrument those two',
202+
});
203+
const task = store.enqueue({
204+
type: 'capture',
205+
dependsOn: [dep.id],
206+
});
207+
const resolved = resolveTask(registry, task, store);
208+
expect(resolved.prompt).toContain('Context from previous steps');
209+
expect(resolved.prompt).toContain('picked signup and purchase');
210+
expect(resolved.prompt).toContain('instrument those two');
211+
});
212+
213+
it('omits the context section when there are no handoffs', () => {
214+
const registry = registryOf([prompt]);
215+
const task = store.enqueue({ type: 'capture' });
216+
expect(resolveTask(registry, task, store).prompt).not.toContain(
217+
'Context from previous steps',
218+
);
219+
});
220+
});
221+
222+
describe('taskModel', () => {
223+
const prompt = parseAgentPrompt(
224+
'---\nmodel: prompt-model\n---\nx',
225+
'capture',
226+
);
227+
228+
it('prefers the enqueue override, then the prompt, then the default', () => {
229+
const registry = registryOf([prompt]);
230+
const task = { type: 'capture' };
231+
expect(taskModel(registry, { ...task, model: 'override' } as never)).toBe(
232+
'override',
233+
);
234+
expect(taskModel(registry, task as never)).toBe('prompt-model');
235+
expect(taskModel(registryOf([]), task as never)).toBe('claude-sonnet-4-6');
236+
});
237+
});
238+
239+
describe('assembleTaskPrompt', () => {
240+
const ctx: OrchestratorPromptContext = {
241+
projectId: 1,
242+
projectApiKey: 'phc_x',
243+
host: 'https://us.posthog.com',
244+
};
245+
246+
it('points the agent at its installed task instructions', () => {
247+
const assembled = assembleTaskPrompt(ctx, 'do the task', [
248+
'.posthog-wizard/skills/capture/SKILL.md',
249+
]);
250+
expect(assembled).toContain('.posthog-wizard/skills/capture/SKILL.md');
251+
expect(assembled).toContain('do the task');
252+
});
253+
254+
it('omits the instructions section when no skills are installed', () => {
255+
expect(assembleTaskPrompt(ctx, 'do the task')).not.toContain(
256+
'task instructions',
257+
);
258+
});
259+
});

0 commit comments

Comments
 (0)