skimpyclaw 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1225,3 +1225,22 @@ describe('Skills endpoints', () => {
1225
1225
  expect(res.json()).toHaveProperty('deleted', true);
1226
1226
  });
1227
1227
  });
1228
+ describe('Office endpoints', () => {
1229
+ it('GET /api/dashboard/office-status returns office status', async () => {
1230
+ const res = await inject({ method: 'GET', url: '/api/dashboard/office-status' });
1231
+ expect(res.statusCode).toBe(200);
1232
+ const json = res.json();
1233
+ expect(json).toHaveProperty('state');
1234
+ expect(json).toHaveProperty('currentTask');
1235
+ expect(json).toHaveProperty('updatedAt');
1236
+ expect(['working', 'thinking', 'waiting', 'offline']).toContain(json.state);
1237
+ });
1238
+ it('GET /api/dashboard/office-scene.png returns image or 404', async () => {
1239
+ const res = await inject({ method: 'GET', url: '/api/dashboard/office-scene.png' });
1240
+ // In test env the image file may not exist, so accept 200 or 404
1241
+ expect([200, 404]).toContain(res.statusCode);
1242
+ if (res.statusCode === 200) {
1243
+ expect(res.headers['content-type']).toBe('image/png');
1244
+ }
1245
+ });
1246
+ });
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken, mockCheckBrowserBinaryIfEnabled, mockCheckVoiceDependencies, mockCheckMcpConfig, mockCheckGatewayHostBindable, mockCheckSkimpyclawDirWritable, mockCheckPortAvailability, mockCheckSandboxAvailable, } = vi.hoisted(() => ({
2
+ const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken, mockCheckBrowserBinaryIfEnabled, mockCheckPlaywrightIfBrowserEnabled, mockCheckVoiceDependencies, mockCheckMcpConfig, mockCheckGatewayHostBindable, mockCheckSkimpyclawDirWritable, mockCheckPortAvailability, mockCheckSandboxAvailable, } = vi.hoisted(() => ({
3
3
  mockLoadConfig: vi.fn(),
4
4
  mockCheckNodeVersion: vi.fn(),
5
5
  mockCheckPackageManagerAvailable: vi.fn(),
@@ -12,6 +12,7 @@ const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable,
12
12
  mockCheckTelegramToken: vi.fn(),
13
13
  mockCheckDiscordToken: vi.fn(),
14
14
  mockCheckBrowserBinaryIfEnabled: vi.fn(),
15
+ mockCheckPlaywrightIfBrowserEnabled: vi.fn(),
15
16
  mockCheckVoiceDependencies: vi.fn(),
16
17
  mockCheckMcpConfig: vi.fn(),
17
18
  mockCheckGatewayHostBindable: vi.fn(),
@@ -34,6 +35,7 @@ vi.mock('../doctor/checks.js', () => ({
34
35
  checkTelegramToken: mockCheckTelegramToken,
35
36
  checkDiscordToken: mockCheckDiscordToken,
36
37
  checkBrowserBinaryIfEnabled: mockCheckBrowserBinaryIfEnabled,
38
+ checkPlaywrightIfBrowserEnabled: mockCheckPlaywrightIfBrowserEnabled,
37
39
  checkVoiceDependencies: mockCheckVoiceDependencies,
38
40
  checkMcpConfig: mockCheckMcpConfig,
39
41
  checkGatewayHostBindable: mockCheckGatewayHostBindable,
@@ -75,6 +77,7 @@ describe('doctor runner', () => {
75
77
  mockCheckTelegramToken.mockReset();
76
78
  mockCheckDiscordToken.mockReset();
77
79
  mockCheckBrowserBinaryIfEnabled.mockReset();
80
+ mockCheckPlaywrightIfBrowserEnabled.mockReset();
78
81
  mockCheckVoiceDependencies.mockReset();
79
82
  mockCheckMcpConfig.mockReset();
80
83
  mockCheckGatewayHostBindable.mockReset();
@@ -92,6 +95,7 @@ describe('doctor runner', () => {
92
95
  mockCheckTelegramToken.mockResolvedValue(okCheck('telegram_token_valid', 'channels'));
93
96
  mockCheckDiscordToken.mockResolvedValue(okCheck('discord_token_valid', 'channels'));
94
97
  mockCheckBrowserBinaryIfEnabled.mockResolvedValue(okCheck('browser_binary_available', 'runtime'));
98
+ mockCheckPlaywrightIfBrowserEnabled.mockResolvedValue(okCheck('playwright_installed', 'runtime', 'Browser tools disabled'));
95
99
  mockCheckVoiceDependencies.mockResolvedValue(okCheck('voice_dependencies', 'runtime', 'Voice disabled'));
96
100
  mockCheckMcpConfig.mockResolvedValue(okCheck('mcp_config', 'runtime', 'MCP tools not configured'));
97
101
  mockCheckGatewayHostBindable.mockResolvedValue(okCheck('gateway_host_bindable', 'runtime', '127.0.0.1 (always available)'));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
2
+ import { join } from 'path';
3
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ // Import the real functions (no mocks needed — these are pure fs reads)
6
+ import { detectPackageManager, buildValidationCommand } from '../code-agents/executor.js';
7
+ describe('detectPackageManager', () => {
8
+ let tempDir;
9
+ beforeEach(() => {
10
+ tempDir = mkdtempSync(join(tmpdir(), 'pm-detect-'));
11
+ });
12
+ afterEach(() => {
13
+ rmSync(tempDir, { recursive: true, force: true });
14
+ });
15
+ it('returns pnpm as fallback when no indicators present', () => {
16
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
17
+ });
18
+ it('detects yarn from packageManager field', () => {
19
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
20
+ name: 'test',
21
+ packageManager: 'yarn@4.1.0',
22
+ }));
23
+ expect(detectPackageManager(tempDir)).toBe('yarn');
24
+ });
25
+ it('detects pnpm from packageManager field', () => {
26
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
27
+ name: 'test',
28
+ packageManager: 'pnpm@9.0.0',
29
+ }));
30
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
31
+ });
32
+ it('detects npm from packageManager field', () => {
33
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
34
+ name: 'test',
35
+ packageManager: 'npm@10.0.0',
36
+ }));
37
+ expect(detectPackageManager(tempDir)).toBe('npm');
38
+ });
39
+ it('detects bun from packageManager field', () => {
40
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
41
+ name: 'test',
42
+ packageManager: 'bun@1.0.0',
43
+ }));
44
+ expect(detectPackageManager(tempDir)).toBe('bun');
45
+ });
46
+ it('detects yarn from yarn.lock', () => {
47
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
48
+ writeFileSync(join(tempDir, 'yarn.lock'), '');
49
+ expect(detectPackageManager(tempDir)).toBe('yarn');
50
+ });
51
+ it('detects pnpm from pnpm-lock.yaml', () => {
52
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
53
+ writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
54
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
55
+ });
56
+ it('detects npm from package-lock.json', () => {
57
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
58
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
59
+ expect(detectPackageManager(tempDir)).toBe('npm');
60
+ });
61
+ it('detects bun from bun.lockb', () => {
62
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
63
+ writeFileSync(join(tempDir, 'bun.lockb'), '');
64
+ expect(detectPackageManager(tempDir)).toBe('bun');
65
+ });
66
+ it('detects bun from bun.lock', () => {
67
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({ name: 'test' }));
68
+ writeFileSync(join(tempDir, 'bun.lock'), '');
69
+ expect(detectPackageManager(tempDir)).toBe('bun');
70
+ });
71
+ it('packageManager field takes priority over lockfile', () => {
72
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
73
+ name: 'test',
74
+ packageManager: 'yarn@4.1.0',
75
+ }));
76
+ // Also has a pnpm lockfile — packageManager should win
77
+ writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
78
+ expect(detectPackageManager(tempDir)).toBe('yarn');
79
+ });
80
+ it('handles malformed package.json gracefully', () => {
81
+ writeFileSync(join(tempDir, 'package.json'), 'not valid json{{{');
82
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
83
+ });
84
+ // wp-calypso case: yarn project with yarn.lock
85
+ it('detects yarn for wp-calypso-like project', () => {
86
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
87
+ name: 'wp-calypso',
88
+ scripts: { build: 'yarn build:client', test: 'jest' },
89
+ }));
90
+ writeFileSync(join(tempDir, 'yarn.lock'), '# yarn lockfile v1\n');
91
+ expect(detectPackageManager(tempDir)).toBe('yarn');
92
+ });
93
+ });
94
+ describe('buildValidationCommand', () => {
95
+ let tempDir;
96
+ beforeEach(() => {
97
+ tempDir = mkdtempSync(join(tmpdir(), 'pm-cmd-'));
98
+ });
99
+ afterEach(() => {
100
+ rmSync(tempDir, { recursive: true, force: true });
101
+ });
102
+ it('returns pnpm build && pnpm test for pnpm project with both scripts', () => {
103
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
104
+ name: 'test',
105
+ scripts: { build: 'tsc', test: 'vitest' },
106
+ }));
107
+ writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
108
+ expect(buildValidationCommand(tempDir)).toBe('pnpm build && pnpm test');
109
+ });
110
+ it('returns yarn build && yarn test for yarn project with both scripts', () => {
111
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
112
+ name: 'test',
113
+ scripts: { build: 'tsc', test: 'jest' },
114
+ }));
115
+ writeFileSync(join(tempDir, 'yarn.lock'), '');
116
+ expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
117
+ });
118
+ it('returns npm run build && npm run test for npm project with both scripts', () => {
119
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
120
+ name: 'test',
121
+ scripts: { build: 'tsc', test: 'jest' },
122
+ }));
123
+ writeFileSync(join(tempDir, 'package-lock.json'), '{}');
124
+ expect(buildValidationCommand(tempDir)).toBe('npm run build && npm run test');
125
+ });
126
+ it('returns bun build && bun test for bun project with both scripts', () => {
127
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
128
+ name: 'test',
129
+ scripts: { build: 'bun build', test: 'bun test' },
130
+ }));
131
+ writeFileSync(join(tempDir, 'bun.lockb'), '');
132
+ expect(buildValidationCommand(tempDir)).toBe('bun build && bun test');
133
+ });
134
+ it('returns only test command when build script is missing', () => {
135
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
136
+ name: 'test',
137
+ scripts: { test: 'jest' },
138
+ }));
139
+ writeFileSync(join(tempDir, 'yarn.lock'), '');
140
+ expect(buildValidationCommand(tempDir)).toBe('yarn test');
141
+ });
142
+ it('returns only build command when test script is missing', () => {
143
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
144
+ name: 'test',
145
+ scripts: { build: 'tsc' },
146
+ }));
147
+ writeFileSync(join(tempDir, 'yarn.lock'), '');
148
+ expect(buildValidationCommand(tempDir)).toBe('yarn build');
149
+ });
150
+ it('falls back to both commands when no package.json exists', () => {
151
+ // No package.json, no lockfile → pnpm fallback
152
+ expect(buildValidationCommand(tempDir)).toBe('pnpm build && pnpm test');
153
+ });
154
+ it('falls back to both commands when scripts object is empty', () => {
155
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
156
+ name: 'test',
157
+ scripts: {},
158
+ }));
159
+ writeFileSync(join(tempDir, 'yarn.lock'), '');
160
+ expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
161
+ });
162
+ // wp-calypso scenario
163
+ it('generates yarn commands for wp-calypso-like project', () => {
164
+ writeFileSync(join(tempDir, 'package.json'), JSON.stringify({
165
+ name: 'wp-calypso',
166
+ packageManager: 'yarn@4.5.0',
167
+ scripts: { build: 'yarn workspaces foreach build', test: 'jest' },
168
+ }));
169
+ writeFileSync(join(tempDir, 'yarn.lock'), '# yarn lockfile v1\n');
170
+ expect(buildValidationCommand(tempDir)).toBe('yarn build && yarn test');
171
+ });
172
+ });
package/dist/api.js CHANGED
@@ -785,6 +785,33 @@ export function registerDashboardAPI(fastify, config) {
785
785
  }
786
786
  return { denied: true, id, command: approval.command };
787
787
  });
788
+ // --- Office Status ---
789
+ fastify.get('/api/dashboard/office-status', async (request) => {
790
+ // Phase 1: return mock data
791
+ return {
792
+ state: 'working',
793
+ currentTask: 'Processing dashboard updates',
794
+ updatedAt: new Date().toISOString(),
795
+ };
796
+ });
797
+ // --- Office Scene Image ---
798
+ fastify.get('/api/dashboard/office-scene.png', async (_request, reply) => {
799
+ const home = homedir();
800
+ const candidates = [
801
+ join(home, '.skimpyclaw', 'office-preview-check.png'),
802
+ join(home, '.skimpyclaw', 'office-offline.png'),
803
+ // Container/workspace fallbacks (e.g. Codespaces, Docker where HOME=/workspace)
804
+ '/workspace/.skimpyclaw/office-preview-check.png',
805
+ '/workspace/.skimpyclaw/office-offline.png',
806
+ ].filter((p, i, arr) => arr.indexOf(p) === i); // dedupe if home IS /workspace
807
+ for (const candidate of candidates) {
808
+ if (existsSync(candidate)) {
809
+ const buffer = readFileSync(candidate);
810
+ return reply.type('image/png').send(buffer);
811
+ }
812
+ }
813
+ return reply.code(404).send({ error: 'Office scene image not found' });
814
+ });
788
815
  // --- Digests ---
789
816
  fastify.get('/api/dashboard/digests', async () => {
790
817
  const digests = getDigests();
package/dist/cli.js CHANGED
@@ -780,10 +780,18 @@ function sandboxImageExists(runtime, image) {
780
780
  return spawnSync(runtime, ['image', 'inspect', image], { encoding: 'utf-8' }).status === 0;
781
781
  }
782
782
  function resolveSandboxDir() {
783
+ // 1. Check CWD (user is in repo root)
783
784
  const cwdSandbox = join(process.cwd(), 'sandbox');
784
785
  if (existsSync(join(cwdSandbox, 'Dockerfile'))) {
785
786
  return cwdSandbox;
786
787
  }
788
+ // 2. Check relative to package root (global/npm install)
789
+ const thisFile = fileURLToPath(import.meta.url);
790
+ const pkgRoot = join(thisFile, '..', '..'); // dist/src/cli.js -> repo root
791
+ const pkgSandbox = join(pkgRoot, 'sandbox');
792
+ if (existsSync(join(pkgSandbox, 'Dockerfile'))) {
793
+ return pkgSandbox;
794
+ }
787
795
  return null;
788
796
  }
789
797
  function parseSandboxOption(args, flag) {
@@ -1,4 +1,20 @@
1
1
  import type { CodeAgentBackgroundOptions, ValidationResult } from './types.js';
2
+ /** Supported JS package managers. */
3
+ export type PackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun';
4
+ /**
5
+ * Detect the package manager for a project directory.
6
+ * Detection order (first match wins):
7
+ * 1. package.json `packageManager` field (e.g. "yarn@4.1.0")
8
+ * 2. Lockfile presence: yarn.lock → yarn, pnpm-lock.yaml → pnpm, bun.lockb / bun.lock → bun, package-lock.json → npm
9
+ * 3. Fallback: 'pnpm' (SkimpyClaw default)
10
+ */
11
+ export declare function detectPackageManager(workdir: string): PackageManager;
12
+ /**
13
+ * Build the validation command for a project directory.
14
+ * Checks for `build` and `test` scripts in package.json, then runs them
15
+ * with the detected package manager. Falls back to `<pm> build && <pm> test`.
16
+ */
17
+ export declare function buildValidationCommand(workdir: string): string;
2
18
  /** Run build/test validation. Shared by solo agents and team orchestrator. */
3
19
  export declare function runValidation(workdir: string): Promise<ValidationResult>;
4
20
  /** Background execution of a coding agent. Updates task status throughout. */
@@ -1,6 +1,6 @@
1
1
  // Code Agent Executor - Background execution logic
2
2
  import { spawn, exec } from 'child_process';
3
- import { createWriteStream } from 'fs';
3
+ import { createWriteStream, existsSync, readFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  // SKIMPYCLAW_ROOT for log paths
6
6
  const SKIMPYCLAW_ROOT = join(import.meta.dirname || process.cwd(), '..', '..');
@@ -12,10 +12,83 @@ import { startTrace, addEvent, endTrace } from '../audit.js';
12
12
  import { buildUsageRecord, recordUsage } from '../usage.js';
13
13
  import { ensureContainer, SANDBOX_DEFAULTS, getRuntime } from '../sandbox/index.js';
14
14
  const CANCELLED_MESSAGE = 'Cancelled by user';
15
+ /**
16
+ * Detect the package manager for a project directory.
17
+ * Detection order (first match wins):
18
+ * 1. package.json `packageManager` field (e.g. "yarn@4.1.0")
19
+ * 2. Lockfile presence: yarn.lock → yarn, pnpm-lock.yaml → pnpm, bun.lockb / bun.lock → bun, package-lock.json → npm
20
+ * 3. Fallback: 'pnpm' (SkimpyClaw default)
21
+ */
22
+ export function detectPackageManager(workdir) {
23
+ // 1. Check package.json packageManager field
24
+ try {
25
+ const pkgPath = join(workdir, 'package.json');
26
+ if (existsSync(pkgPath)) {
27
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
28
+ if (typeof pkg.packageManager === 'string') {
29
+ const name = pkg.packageManager.split('@')[0].toLowerCase();
30
+ if (name === 'yarn')
31
+ return 'yarn';
32
+ if (name === 'pnpm')
33
+ return 'pnpm';
34
+ if (name === 'npm')
35
+ return 'npm';
36
+ if (name === 'bun')
37
+ return 'bun';
38
+ }
39
+ }
40
+ }
41
+ catch { /* ignore parse errors, fall through */ }
42
+ // 2. Check lockfiles
43
+ if (existsSync(join(workdir, 'yarn.lock')))
44
+ return 'yarn';
45
+ if (existsSync(join(workdir, 'pnpm-lock.yaml')))
46
+ return 'pnpm';
47
+ if (existsSync(join(workdir, 'bun.lockb')) || existsSync(join(workdir, 'bun.lock')))
48
+ return 'bun';
49
+ if (existsSync(join(workdir, 'package-lock.json')))
50
+ return 'npm';
51
+ // 3. Fallback
52
+ return 'pnpm';
53
+ }
54
+ /**
55
+ * Build the validation command for a project directory.
56
+ * Checks for `build` and `test` scripts in package.json, then runs them
57
+ * with the detected package manager. Falls back to `<pm> build && <pm> test`.
58
+ */
59
+ export function buildValidationCommand(workdir) {
60
+ const pm = detectPackageManager(workdir);
61
+ const run = pm === 'npm' ? 'npm run' : pm;
62
+ // Check which scripts exist in package.json
63
+ let hasBuild = false;
64
+ let hasTest = false;
65
+ try {
66
+ const pkgPath = join(workdir, 'package.json');
67
+ if (existsSync(pkgPath)) {
68
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
69
+ const scripts = pkg.scripts || {};
70
+ hasBuild = !!scripts.build;
71
+ hasTest = !!scripts.test;
72
+ }
73
+ }
74
+ catch { /* ignore */ }
75
+ const parts = [];
76
+ if (hasBuild)
77
+ parts.push(`${run} build`);
78
+ if (hasTest)
79
+ parts.push(`${run} test`);
80
+ // If neither build nor test scripts exist, still try — the scripts
81
+ // might be defined in a workspace root or the commands may work anyway
82
+ if (parts.length === 0) {
83
+ parts.push(`${run} build`, `${run} test`);
84
+ }
85
+ return parts.join(' && ');
86
+ }
15
87
  /** Run build/test validation. Shared by solo agents and team orchestrator. */
16
88
  export function runValidation(workdir) {
89
+ const cmd = buildValidationCommand(workdir);
17
90
  return new Promise((resolve) => {
18
- exec('pnpm build && pnpm test', {
91
+ exec(cmd, {
19
92
  cwd: workdir,
20
93
  timeout: VALIDATE_TIMEOUT_MS,
21
94
  maxBuffer: 5 * 1024 * 1024,
@@ -242,8 +315,9 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
242
315
  caTask.outputPreview = agentOutput.slice(0, 500);
243
316
  caTask.liveOutput = undefined;
244
317
  writeCodeAgentTask(caTask);
318
+ const validationCmd = buildValidationCommand(workdir);
245
319
  const runValidationPromise = () => new Promise((res) => {
246
- const validationProc = exec('pnpm build && pnpm test', {
320
+ const validationProc = exec(validationCmd, {
247
321
  cwd: workdir,
248
322
  timeout: VALIDATE_TIMEOUT_MS,
249
323
  maxBuffer: 5 * 1024 * 1024,
@@ -2,6 +2,7 @@
2
2
  import { execSync } from 'child_process';
3
3
  import { resolve, join } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { buildValidationCommand } from './executor.js';
5
6
  // Resolve CLI paths once at import time so spawn doesn't get ENOENT
6
7
  function resolveCliPath(name) {
7
8
  try {
@@ -113,7 +114,7 @@ export function buildCodeAgentArgs(input) {
113
114
  '--mcp-config', playwrightMcp,
114
115
  ...toolArgs,
115
116
  '--max-turns', maxTurns,
116
- '--append-system-prompt', 'Output text only. Never use say or TTS. Focus on the coding task. Run pnpm build && pnpm test to verify changes.',
117
+ '--append-system-prompt', `Output text only. Never use say or TTS. Focus on the coding task. Run ${buildValidationCommand(input.workdir || process.cwd())} to verify changes.`,
117
118
  ];
118
119
  // Only pass model to Claude CLI if it's not a known non-Claude model.
119
120
  // GPT/Codex/Kimi/o-series models would be rejected by the Claude CLI.
@@ -152,6 +153,10 @@ export function buildTeamNotification(task, getChildTask) {
152
153
  lines.push(` ${childIcon} ${child.id} (${childDur}): ${subtask}`);
153
154
  }
154
155
  }
156
+ // Synthesis result
157
+ if (task.outputPreview) {
158
+ lines.push(`\nResult: ${task.outputPreview}`);
159
+ }
155
160
  // Errors
156
161
  if (task.error && task.error !== 'Validation failed') {
157
162
  lines.push(`\nError: ${task.error}`);
@@ -169,8 +174,7 @@ export function buildSoloNotification(task) {
169
174
  const validation = task.validationPassed ? ' Build/tests pass.' : '';
170
175
  let message = `✅ Coding agent ${task.id} completed (${dur}).${validation}\n\nTask: ${taskPreview}`;
171
176
  if (task.outputPreview) {
172
- const preview = task.outputPreview.slice(0, 300);
173
- message += `\n\nResult: ${preview}`;
177
+ message += `\n\nResult: ${task.outputPreview}`;
174
178
  }
175
179
  return message;
176
180
  }