Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2046202
chore(main): release 2.39.0
releaser-wizard[bot] Jul 8, 2026
50bb273
feat(pi): orchestrator runTask — per-task pi sessions with in-process…
gewenyu99 Jul 9, 2026
3d65d52
fix(pi): explicit MCP direct-tool list, assistant-only message accoun…
gewenyu99 Jul 10, 2026
34f5268
fix(pi): carry the snake_case event-naming note into task mode
gewenyu99 Jul 10, 2026
a2a3368
Merge remote-tracking branch 'origin/main' into experiment/orchestrat…
gewenyu99 Jul 10, 2026
d69765a
Merge remote-tracking branch 'origin/main' into experiment/orchestrat…
gewenyu99 Jul 14, 2026
8af37b1
feat(orchestrator): terra medium + per-task effort from the model table
gewenyu99 Jul 14, 2026
cb4c5b7
feat(fence): allow .env example/template files through the .env write…
gewenyu99 Jul 14, 2026
d84aaa3
feat(pi): log per-task token usage so a run's cost is observable from…
gewenyu99 Jul 14, 2026
cf22c95
feat(pi): tag per-task usage log with task type and duration
gewenyu99 Jul 14, 2026
7318152
feat(orchestrator): resolve per-agent model + effort by harness profile
gewenyu99 Jul 14, 2026
924e656
fix(orchestrator): resolve framework variants to parity (rails, react…
gewenyu99 Jul 15, 2026
1c49cea
chore: prettier format
gewenyu99 Jul 15, 2026
d09b590
chore: drop unnecessary type assertion (eslint)
gewenyu99 Jul 15, 2026
de41ed5
fix(pi): review fixes — typed effort, live model fallback, menu-decla…
gewenyu99 Jul 15, 2026
5009784
Merge branch 'main' into experiment/orchestrator-pi-runtask
gewenyu99 Jul 15, 2026
970b0c9
feat(orchestrator): fail the run properly on a missing skill variant …
gewenyu99 Jul 15, 2026
f5380c0
Merge branch 'main' into experiment/orchestrator-pi-runtask
gewenyu99 Jul 15, 2026
7fa6e41
feat(orchestrator): integration-v2 flow, fetch retries, PHP/Ruby allo…
gewenyu99 Jul 15, 2026
ac95aed
Merge remote-tracking branch 'origin/main' into experiment/orchestrat…
gewenyu99 Jul 15, 2026
990f1c3
fix(pi): venv guidance + restore .env.example write carve-out
gewenyu99 Jul 15, 2026
e612cf5
feat(commandments): universal rule — never default the project key to…
gewenyu99 Jul 15, 2026
19ffab0
style: prettier formatting for the fence + commandments changes
gewenyu99 Jul 15, 2026
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
45 changes: 45 additions & 0 deletions src/lib/__tests__/wizard-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
__test,
ensureGitignoreCoverage,
evaluateAskCap,
fetchSkillMenu,
mergeEnvValues,
parseEnvKeys,
resolveEnvPath,
Expand Down Expand Up @@ -490,3 +491,47 @@ describe('downloadWithRetry', () => {
).rejects.toThrow(/attempt 1.*attempt 2.*attempt 3/s);
});
});

describe('fetchSkillMenu', () => {
const noSleep = () => Promise.resolve();
const menu = { categories: { integration: [] } };
const menuResponse = () =>
Promise.resolve({
ok: true,
status: 200,
statusText: 'OK',
json: () => Promise.resolve(menu),
});

it('retries a flaky menu fetch before succeeding', async () => {
let attempts = 0;

const result = await fetchSkillMenu('http://localhost:8765', {
fetchImpl: (() => {
attempts += 1;
if (attempts < 3) return Promise.reject(new Error('reset'));
return menuResponse();
}) as any,
sleepImpl: noSleep,
});

expect(attempts).toBe(3);
expect(result).toEqual(menu);
});

it('returns null after exhausting retries', async () => {
let attempts = 0;

const result = await fetchSkillMenu('http://localhost:8765', {
fetchImpl: (() => {
attempts += 1;
return Promise.reject(new Error('network down'));
}) as any,
sleepImpl: noSleep,
maxAttempts: 3,
});

expect(attempts).toBe(3);
expect(result).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`getWizardCommandments > matches the published commandment list 1`] = `
"Never hallucinate a PostHog project token, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables).
Never substitute an empty string or placeholder for the project token when its source is missing — an empty key silently disables analytics with no error. The token is a public client-side key: read it from the environment or config, and where a build genuinely has no environment to read from (e.g. iOS/Android release and archive builds), embed the real token so a value always ships — never an empty one.
Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files.
Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.
Before writing to any file, you MUST read that exact file immediately beforehand using the Read tool, even if you have already read it earlier in the run. This avoids tool failures and stale edits.
Expand Down
100 changes: 82 additions & 18 deletions src/lib/agent/__tests__/agent-prompt-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
assembleTaskPrompt,
buildRegistry,
parseAgentPrompt,
promptModelFor,
resolveTask,
taskModel,
taskModelSpec,
type AgentPrompt,
type AgentRegistry,
type OrchestratorPromptContext,
Expand All @@ -29,7 +30,9 @@ function registryOf(prompts: AgentPrompt[]): AgentRegistry {
describe('parseAgentPrompt', () => {
const sample = `---
type: instrument-events
model: claude-sonnet-4-6 # cheapest model that succeeds
model_pi: openai/gpt-5.6-terra # per-profile model targets
effort_pi: medium
model_sdk: claude-sonnet-4-6
skills: [instrument-events]
allowedTools: [Read, Edit, Grep, Glob, Bash]
disallowedTools: [enqueue_task]
Expand All @@ -43,16 +46,54 @@ Add at least one capture call.
it('parses frontmatter scalars and inline arrays', () => {
const p = parseAgentPrompt(sample, 'fallback');
expect(p.type).toBe('instrument-events');
expect(p.model).toBe('claude-sonnet-4-6');
expect(p.modelPi).toBe('openai/gpt-5.6-terra');
expect(p.effortPi).toBe('medium');
expect(p.modelSdk).toBe('claude-sonnet-4-6');
expect(p.skills).toEqual(['instrument-events']);
expect(p.allowedTools).toEqual(['Read', 'Edit', 'Grep', 'Glob', 'Bash']);
expect(p.disallowedTools).toEqual(['enqueue_task']);
expect(p.dependsOn).toEqual(['init']);
});

it('resolves the per-harness model + effort, not 1:1 across providers', () => {
const p = parseAgentPrompt(sample, 'fallback');
expect(promptModelFor(p, 'pi')).toEqual({
model: 'openai/gpt-5.6-terra',
effort: 'medium',
});
expect(promptModelFor(p, 'anthropic')).toEqual({
model: 'claude-sonnet-4-6',
effort: undefined,
});
});

it('drops an effort that is not a ThinkingLevel — remote typos never reach a session', () => {
const p = parseAgentPrompt(
'---\nmodel_pi: m\neffort_pi: mediun\neffort_sdk: high\n---\nx',
'capture',
);
expect(p.effortPi).toBeUndefined();
expect(p.effortSdk).toBe('high');
});

it('falls back to the menu entry flow when frontmatter omits it', () => {
const p = parseAgentPrompt(
'---\ntype: install\n---\nx',
'install',
'my-flow',
);
expect(p.flow).toBe('my-flow');
const declared = parseAgentPrompt(
'---\nflow: audit\n---\nx',
'install',
'my-flow',
);
expect(declared.flow).toBe('audit');
});

it('strips inline comments and keeps the body', () => {
const p = parseAgentPrompt(sample, 'fallback');
expect(p.model).not.toContain('#');
expect(p.modelPi).not.toContain('#');
expect(p.body).toContain('## Goal');
expect(p.body).not.toContain('---');
});
Expand All @@ -76,9 +117,10 @@ Add at least one capture call.
);
});

it('defaults missing array fields to empty and model to undefined', () => {
it('defaults missing array fields to empty and models to undefined', () => {
const p = parseAgentPrompt('no frontmatter at all', 'stub');
expect(p.model).toBeUndefined();
expect(p.modelPi).toBeUndefined();
expect(p.modelSdk).toBeUndefined();
expect(p.skills).toEqual([]);
expect(p.dependsOn).toEqual([]);
expect(p.body).toBe('no frontmatter at all');
Expand Down Expand Up @@ -121,7 +163,7 @@ describe('buildRegistry', () => {
[
prompt({ type: 'plan-audit', flow: 'audit', seed: true }),
prompt({ type: 'fix-events', flow: 'audit' }),
prompt({ type: 'install', flow: 'posthog-integration' }),
prompt({ type: 'install', flow: 'integration-v2' }),
prompt({ type: 'example' }),
],
'audit',
Expand Down Expand Up @@ -162,7 +204,9 @@ describe('resolveTask', () => {
const prompt: AgentPrompt = {
type: 'capture',
seed: false,
model: 'claude-haiku-4-5-20251001',
modelPi: 'openai/gpt-5.6-luna',
effortPi: 'low',
modelSdk: 'claude-haiku-4-5-20251001',
skills: ['instrument-events'],
allowedTools: ['Read', 'Edit'],
disallowedTools: ['enqueue_task'],
Expand All @@ -176,21 +220,32 @@ describe('resolveTask', () => {
expect(() => resolveTask(registry, task, store)).toThrow(/capture/);
});

it('resolves model, tools, and skills from the prompt', () => {
it('resolves tools and skills from the prompt', () => {
const registry = registryOf([prompt]);
const task = store.enqueue({ type: 'capture' });
const resolved = resolveTask(registry, task, store);
expect(resolved.model).toBe('claude-haiku-4-5-20251001');
expect(resolved.skills).toEqual(['instrument-events']);
expect(resolved.disallowedTools).toEqual([
'mcp__posthog-wizard__enqueue_task',
]);
});

it('resolves per-harness model + effort from the prompt', () => {
const registry = registryOf([prompt]);
const task = store.enqueue({ type: 'capture' });
expect(taskModelSpec(registry, task, 'pi')).toEqual({
model: 'openai/gpt-5.6-luna',
effort: 'low',
});
expect(taskModelSpec(registry, task, 'anthropic').model).toBe(
'claude-haiku-4-5-20251001',
);
});

it('prefers the enqueue model override over the prompt model', () => {
const registry = registryOf([prompt]);
const task = store.enqueue({ type: 'capture', model: 'override-x' });
expect(resolveTask(registry, task, store).model).toBe('override-x');
expect(taskModelSpec(registry, task, 'pi').model).toBe('override-x');
});

it("appends upstream dependencies' handoffs as context", () => {
Expand Down Expand Up @@ -260,20 +315,29 @@ describe('resolveTask', () => {
});
});

describe('taskModel', () => {
describe('taskModelSpec', () => {
const prompt = parseAgentPrompt(
'---\nmodel: prompt-model\n---\nx',
'---\nmodel_pi: prompt-model\n---\nx',
'capture',
);

it('prefers the enqueue override, then the prompt, then the default', () => {
it('prefers the enqueue override, then the prompt; the switchboard pick is the caller fallback', () => {
const registry = registryOf([prompt]);
const task = { type: 'capture' };
expect(taskModel(registry, { ...task, model: 'override' } as never)).toBe(
'override',
expect(
taskModelSpec(registry, { ...task, model: 'override' } as never, 'pi')
.model,
).toBe('override');
expect(taskModelSpec(registry, task as never, 'pi').model).toBe(
'prompt-model',
);
expect(taskModel(registry, task as never)).toBe('prompt-model');
expect(taskModel(registryOf([]), task as never)).toBe('claude-sonnet-4-6');
// An empty column stays undefined — the caller falls back to its switchboard pick.
expect(
taskModelSpec(registry, task as never, 'anthropic').model,
).toBeUndefined();
expect(
taskModelSpec(registryOf([]), task as never, 'pi').model,
).toBeUndefined();
});
});

Expand Down
6 changes: 3 additions & 3 deletions src/lib/agent/__tests__/variant-gating.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ describe('isOrchestratorEnabled', () => {
describe('pi + orchestrator gating', () => {
const program = 'posthog-integration' as const;

it('clamps the sequence to linear when both flags select pi + orchestrator', () => {
// pi has no runTask — the clamp forces linear.
it('runs the orchestrator on pi when both flags select pi + orchestrator', () => {
// pi implements runTask — the capability clamp passes and the flag stands.
const binding = resolveBinding({
program,
flags: {
Expand All @@ -39,7 +39,7 @@ describe('pi + orchestrator gating', () => {
},
});
expect(binding.harness).toBe(Harness.pi);
expect(binding.sequence).toBe(Sequence.linear);
expect(binding.sequence).toBe(Sequence.orchestrator);
});

it('leaves the orchestrator flag effective for the anthropic harness', () => {
Expand Down
9 changes: 7 additions & 2 deletions src/lib/agent/agent-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,16 @@ export function wizardCanUseTool(
};
}

// Block direct reads/writes of .env files — use wizard-tools MCP instead
// Block direct reads/writes of real .env files — use wizard-tools MCP instead.
// Example/template files (.env.example, .env.sample, .env.template, .env.dist)
// carry no secrets and are meant to be committed, so they stay writable.
if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') {
const filePath = typeof input.file_path === 'string' ? input.file_path : '';
const basename = path.basename(filePath);
if (basename.startsWith('.env')) {
const isEnvExample = /^\.env\.(example|sample|template|dist)$/.test(
basename,
);
if (basename.startsWith('.env') && !isEnvExample) {
logToFile(`Denying ${toolName} on env file: ${filePath}`);
return {
behavior: 'deny',
Expand Down
Loading
Loading