runwork 0.10.3 → 0.11.0

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.
Files changed (44) hide show
  1. package/dist/agents/__tests__/claude-code-managed-block.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-managed-block.test.js +97 -0
  3. package/dist/agents/__tests__/claude-code-stats.test.js +1 -0
  4. package/dist/agents/__tests__/codex-minimum-permissions.test.d.ts +1 -0
  5. package/dist/agents/__tests__/codex-minimum-permissions.test.js +82 -0
  6. package/dist/agents/__tests__/cursor-merge.test.d.ts +1 -0
  7. package/dist/agents/__tests__/cursor-merge.test.js +66 -0
  8. package/dist/agents/__tests__/defaults-merge.test.d.ts +1 -0
  9. package/dist/agents/__tests__/defaults-merge.test.js +268 -0
  10. package/dist/agents/claude-code.d.ts +5 -0
  11. package/dist/agents/claude-code.js +30 -1
  12. package/dist/agents/cursor.d.ts +23 -1
  13. package/dist/agents/cursor.js +43 -4
  14. package/dist/agents/default-config.d.ts +30 -0
  15. package/dist/agents/default-config.js +67 -0
  16. package/dist/agents/defaults-merge.d.ts +72 -0
  17. package/dist/agents/defaults-merge.js +131 -0
  18. package/dist/agents/types.d.ts +31 -2
  19. package/dist/commands/clone.js +1 -1
  20. package/dist/commands/deploy.js +1 -1
  21. package/dist/commands/dev.js +22 -18
  22. package/dist/commands/init.js +1 -1
  23. package/dist/commands/sync.js +146 -59
  24. package/dist/dev/__tests__/detach.test.js +77 -1
  25. package/dist/dev/detach.d.ts +23 -0
  26. package/dist/dev/detach.js +45 -0
  27. package/dist/generated/version.d.ts +1 -1
  28. package/dist/generated/version.js +1 -1
  29. package/dist/git/__tests__/credentials.test.js +1 -1
  30. package/dist/git/auto-commit.js +1 -1
  31. package/dist/git/credentials.js +1 -1
  32. package/dist/git/identity.js +1 -1
  33. package/dist/git/preflight.js +1 -1
  34. package/dist/git/sync.js +1 -1
  35. package/dist/health/checks.js +1 -1
  36. package/dist/template/manifest.js +1 -1
  37. package/dist/types.d.ts +23 -0
  38. package/dist/ui/banner.js +1 -1
  39. package/dist/utils/agent-guidance.d.ts +13 -0
  40. package/dist/utils/agent-guidance.js +22 -7
  41. package/dist/utils/subprocess.d.ts +19 -0
  42. package/dist/utils/subprocess.js +27 -0
  43. package/dist/utils/which.js +1 -1
  44. package/package.json +1 -1
@@ -7,6 +7,8 @@ import { ApiClient } from '../api/client.js';
7
7
  import { getAdapterBySlug, detectAgents } from '../agents/detect.js';
8
8
  import { CodexAdapter } from '../agents/codex.js';
9
9
  import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
10
+ import { RUNWORK_AGENT_DEFAULTS, AGENT_DEFAULTS_SCHEMA_VERSION } from '../agents/default-config.js';
11
+ import { resolveAgentDefaults } from '../agents/defaults-merge.js';
10
12
  import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
11
13
  import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
12
14
  import { computeSyncPlan } from '../sync/change-detect.js';
@@ -346,85 +348,170 @@ export async function syncFromState(state, statePath, credentials, opts) {
346
348
  }
347
349
  }
348
350
  }
349
- // Pull and apply team instructions from onboarding config (non-fatal)
351
+ // Pull team config from server. A failure here must not stop the
352
+ // unified user-scope write below, which applies network and minimum-permission
353
+ // floors regardless of whether team config was fetched.
350
354
  let teamInstructionsApplied = false;
351
355
  let agentConfigsApplied = 0;
356
+ let teamInstructions;
357
+ let agentConfigs;
352
358
  try {
353
359
  const onboardingConfig = await client.getOnboardingConfig(state.workspaceId);
354
- const teamInstructions = onboardingConfig?.config?.teamInstructions;
355
- const agentConfigs = onboardingConfig?.config?.agentConfigs;
356
- // Write team instructions and/or per-agent extra instructions.
357
- // Either or both may be present independently.
360
+ teamInstructions = onboardingConfig?.config?.teamInstructions ?? undefined;
361
+ agentConfigs = onboardingConfig?.config?.agentConfigs ?? undefined;
362
+ }
363
+ catch {
364
+ // Team config pull is non-fatal — proceed with no team-managed overrides.
365
+ }
366
+ // Write team instructions and/or per-agent extra instructions.
367
+ for (const adapter of adapters) {
368
+ if (!adapter.writeTeamInstructions)
369
+ continue;
370
+ const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
371
+ const parts = [teamInstructions, agentExtra].filter(Boolean);
372
+ if (parts.length === 0)
373
+ continue;
374
+ const fullInstructions = parts.join('\n\n');
375
+ for (const scope of scopes) {
376
+ try {
377
+ await adapter.writeTeamInstructions(fullInstructions, scope);
378
+ teamInstructionsApplied = true;
379
+ console.log(` [${adapter.name}] Updated team instructions (${scope})`);
380
+ }
381
+ catch {
382
+ // Best-effort per adapter/scope
383
+ }
384
+ }
385
+ }
386
+ // Apply per-agent project-scope config (team overrides only).
387
+ // User scope is handled below in the unified defaults+network+minimum block.
388
+ if (agentConfigs) {
358
389
  for (const adapter of adapters) {
359
- if (!adapter.writeTeamInstructions)
390
+ const agentConfig = agentConfigs[adapter.slug];
391
+ if (!agentConfig || !adapter.writeAgentConfig)
360
392
  continue;
361
- const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
362
- const parts = [teamInstructions, agentExtra].filter(Boolean);
363
- if (parts.length === 0)
393
+ const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
394
+ if (Object.keys(configWithoutInstructions).length === 0)
364
395
  continue;
365
- const fullInstructions = parts.join('\n\n');
366
396
  for (const scope of scopes) {
397
+ if (scope === 'user')
398
+ continue; // handled in unified block below
367
399
  try {
368
- await adapter.writeTeamInstructions(fullInstructions, scope);
369
- teamInstructionsApplied = true;
370
- console.log(` [${adapter.name}] Updated team instructions (${scope})`);
400
+ await adapter.writeAgentConfig(configWithoutInstructions, scope);
401
+ agentConfigsApplied++;
402
+ const configKeys = Object.keys(configWithoutInstructions).join(', ');
403
+ console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
371
404
  }
372
405
  catch {
373
406
  // Best-effort per adapter/scope
374
407
  }
375
408
  }
376
409
  }
377
- // Apply per-agent config overrides (model preferences, permissions)
378
- if (agentConfigs) {
379
- for (const adapter of adapters) {
380
- const agentConfig = agentConfigs[adapter.slug];
381
- if (!agentConfig || !adapter.writeAgentConfig)
382
- continue;
383
- // Only apply model/permission config (extraInstructions already handled above)
384
- const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
385
- if (Object.keys(configWithoutInstructions).length === 0)
386
- continue;
387
- for (const scope of scopes) {
388
- try {
389
- await adapter.writeAgentConfig(configWithoutInstructions, scope);
390
- agentConfigsApplied++;
391
- const configKeys = Object.keys(configWithoutInstructions).join(', ');
392
- console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
410
+ }
411
+ // Unified user-scope write: merge baked defaults with team config, network
412
+ // allowlist, and minimum permissions into a single writeAgentConfig call
413
+ // per adapter. The merge layer tracks injected defaults in state.agentDefaults
414
+ // so user-removed entries become sticky opt-outs.
415
+ if (scopes.includes('user')) {
416
+ const networkDomains = ['runwork.ai', '*.runwork.ai'];
417
+ try {
418
+ const baseHost = new URL(baseUrl).hostname;
419
+ if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
420
+ networkDomains.push(baseHost, `*.${baseHost}`);
421
+ }
422
+ }
423
+ catch { /* use defaults */ }
424
+ // Order arrays list each agent's valid values from most restrictive to most
425
+ // permissive. The floor logic upgrades anything strictly below `minimum`,
426
+ // including unknown values (-1 < minimumIdx). That makes it critical to
427
+ // include the agent's most-permissive option in `order` — otherwise a user
428
+ // who explicitly set, say, `sandbox_mode = "danger-full-access"` gets
429
+ // silently downgraded to `workspace-write`.
430
+ const minimumPermissions = [
431
+ { field: 'approval_policy', order: ['untrusted', 'on-request', 'on-failure', 'never'], minimum: 'on-request' },
432
+ { field: 'sandbox_mode', order: ['read-only', 'workspace-write', 'danger-full-access'], minimum: 'workspace-write' },
433
+ ];
434
+ // agentDefaults lifecycle: full `runwork uninstall` deletes ~/.runwork
435
+ // (state and all per-agent defaults state with it). Re-running `runwork
436
+ // setup` overwrites setup.json without agentDefaults so every remaining
437
+ // tool bootstraps fresh on the next sync. refreshConfiguredAgents only
438
+ // adds slugs (never removes), so we don't need orphan-entry pruning here.
439
+ if (!state.agentDefaults)
440
+ state.agentDefaults = {};
441
+ for (const adapter of adapters) {
442
+ if (!adapter.writeAgentConfig)
443
+ continue;
444
+ const slug = adapter.slug;
445
+ const baked = RUNWORK_AGENT_DEFAULTS[slug];
446
+ const agentState = state.agentDefaults[slug];
447
+ const team = agentConfigs?.[slug];
448
+ // onDisk = undefined signals "removal detection is not possible" — used for
449
+ // markerless adapters (Cursor) and for any adapter without readManagedBlock.
450
+ // The resolver treats undefined differently from an empty array: it skips
451
+ // opt-out fabrication entirely and trusts the baseline mechanism in the
452
+ // adapter to preserve user edits via subtraction.
453
+ let onDisk;
454
+ if (baked && adapter.readManagedBlock) {
455
+ try {
456
+ onDisk = await adapter.readManagedBlock('user');
457
+ }
458
+ catch {
459
+ onDisk = undefined;
460
+ }
461
+ }
462
+ const resolved = resolveAgentDefaults({
463
+ baked,
464
+ state: agentState,
465
+ onDisk,
466
+ team: team?.permissionRules,
467
+ });
468
+ const mergedConfig = {
469
+ modelPreference: team?.modelPreference,
470
+ permissionRules: (resolved.managedAllow.length || resolved.managedDeny.length || team?.permissionRules?.defaultMode)
471
+ ? {
472
+ ...(resolved.managedAllow.length ? { allow: resolved.managedAllow } : {}),
473
+ ...(resolved.managedDeny.length ? { deny: resolved.managedDeny } : {}),
474
+ ...(team?.permissionRules?.defaultMode ? { defaultMode: team.permissionRules.defaultMode } : {}),
393
475
  }
394
- catch {
395
- // Best-effort per adapter/scope
476
+ : undefined,
477
+ networkAllowlist: networkDomains,
478
+ minimumPermissions,
479
+ };
480
+ try {
481
+ const baseline = agentState?.lastInjected;
482
+ await adapter.writeAgentConfig(mergedConfig, 'user', baseline);
483
+ if (baked) {
484
+ const nextState = {
485
+ lastInjected: {
486
+ allow: resolved.applicableAllow,
487
+ deny: resolved.applicableDeny,
488
+ },
489
+ userOptOuts: {
490
+ allow: resolved.newOptOutsAllow,
491
+ deny: resolved.newOptOutsDeny,
492
+ },
493
+ };
494
+ state.agentDefaults[slug] = nextState;
495
+ // Diagnostic log line (per "invisible / sync log only" policy)
496
+ if (resolved.bootstrapped) {
497
+ console.log(` [${adapter.name}] Bootstrapped default-rule tracking (defaults apply on next sync)`);
498
+ }
499
+ else if (resolved.applicableAllow.length || resolved.applicableDeny.length || resolved.removalsThisSync.allow || resolved.removalsThisSync.deny) {
500
+ const totalRemovals = resolved.removalsThisSync.allow + resolved.removalsThisSync.deny;
501
+ const removalNote = totalRemovals > 0
502
+ ? ` (${totalRemovals} new opt-out${totalRemovals > 1 ? 's' : ''} honored)`
503
+ : '';
504
+ console.log(` [${adapter.name}] Applied defaults: ${resolved.applicableAllow.length} allow, ${resolved.applicableDeny.length} deny${removalNote}`);
396
505
  }
397
506
  }
507
+ if (team)
508
+ agentConfigsApplied++;
509
+ }
510
+ catch {
511
+ // Best-effort per adapter
398
512
  }
399
513
  }
400
- }
401
- catch {
402
- // Team config pull is non-fatal
403
- }
404
- // Ensure sandbox network access for agents that run in sandboxed environments (e.g. Cursor)
405
- const networkDomains = ['runwork.ai', '*.runwork.ai'];
406
- try {
407
- const baseHost = new URL(baseUrl).hostname;
408
- if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
409
- networkDomains.push(baseHost, `*.${baseHost}`);
410
- }
411
- }
412
- catch { /* use defaults */ }
413
- for (const adapter of adapters) {
414
- if (!adapter.writeAgentConfig)
415
- continue;
416
- try {
417
- await adapter.writeAgentConfig({
418
- networkAllowlist: networkDomains,
419
- minimumPermissions: [
420
- { field: 'approval_policy', order: ['always', 'untrusted', 'on-request', 'never'], minimum: 'on-request' },
421
- { field: 'sandbox_mode', order: ['full', 'read-only', 'workspace-write', 'off'], minimum: 'workspace-write' },
422
- ],
423
- }, 'user');
424
- }
425
- catch {
426
- // Best-effort
427
- }
514
+ state.agentDefaultsVersion = AGENT_DEFAULTS_SCHEMA_VERSION;
428
515
  }
429
516
  // Register ~/.runwork as a project in the Codex desktop app (best-effort).
430
517
  // Only attempts when Codex adapter is configured and the desktop app is closed.
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
- import { pollForSession, runAsDetachedParent, isInternalDetachedChild, stripInternalChildFlag, INTERNAL_DETACHED_CHILD_FLAG, } from '../detach.js';
5
+ import { buildChildArgs, pollForSession, runAsDetachedParent, isInternalDetachedChild, looksLikeBunStandaloneArtifact, stripInternalChildFlag, INTERNAL_DETACHED_CHILD_FLAG, } from '../detach.js';
6
6
  import { buildSessionFile, getSessionPaths, writeSessionFile, } from '../session.js';
7
7
  function createTmpAppDir() {
8
8
  return fs.mkdtempSync(path.join(os.tmpdir(), 'runwork-detach-test-'));
@@ -32,6 +32,82 @@ describe('isInternalDetachedChild() / stripInternalChildFlag()', () => {
32
32
  expect(stripInternalChildFlag(['dev', '--detach'])).toEqual(['dev', '--detach']);
33
33
  });
34
34
  });
35
+ describe('looksLikeBunStandaloneArtifact()', () => {
36
+ it('detects Bun-on-Windows virtual-FS paths (forward slashes)', () => {
37
+ expect(looksLikeBunStandaloneArtifact('B:/~BUN/root/runwork-windows-x64.exe')).toBe(true);
38
+ expect(looksLikeBunStandaloneArtifact('C:/~BUN/root/foo.exe')).toBe(true);
39
+ });
40
+ it('detects Bun-on-Windows virtual-FS paths (backslashes)', () => {
41
+ expect(looksLikeBunStandaloneArtifact('B:\\~BUN\\root\\runwork.exe')).toBe(true);
42
+ });
43
+ it('detects bunfs prefix on POSIX (defensive)', () => {
44
+ expect(looksLikeBunStandaloneArtifact('/$bunfs/root/runwork')).toBe(true);
45
+ });
46
+ it('does not flag normal paths or arguments', () => {
47
+ expect(looksLikeBunStandaloneArtifact('dev')).toBe(false);
48
+ expect(looksLikeBunStandaloneArtifact('--detach')).toBe(false);
49
+ expect(looksLikeBunStandaloneArtifact('/usr/local/bin/runwork')).toBe(false);
50
+ expect(looksLikeBunStandaloneArtifact('C:/Users/Oytun/.runwork/bin/runwork.exe')).toBe(false);
51
+ expect(looksLikeBunStandaloneArtifact('node')).toBe(false);
52
+ });
53
+ });
54
+ describe('buildChildArgs()', () => {
55
+ it('Node script invocation: forwards script path + user args, appends marker', () => {
56
+ // node /path/to/dist/index.js dev --detach
57
+ const argv = ['node', '/path/to/dist/index.js', 'dev', '--detach'];
58
+ expect(buildChildArgs(argv)).toEqual([
59
+ '/path/to/dist/index.js', 'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
60
+ ]);
61
+ });
62
+ it('Bun standalone macOS/Linux: forwards user args directly', () => {
63
+ // /usr/local/bin/runwork dev --detach
64
+ const argv = ['/usr/local/bin/runwork', 'dev', '--detach'];
65
+ expect(buildChildArgs(argv)).toEqual([
66
+ 'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
67
+ ]);
68
+ });
69
+ it('Bun standalone Windows: drops the virtual-FS argv[1] before forwarding', () => {
70
+ // The exact failure from the field. Bun-on-Windows injects argv[1]
71
+ // = "B:/~BUN/root/runwork-windows-x64.exe". We must NOT forward it
72
+ // -- the child Bun runtime will inject its own.
73
+ const argv = [
74
+ 'C:\\Users\\Oytun\\.runwork\\bin\\runwork.exe',
75
+ 'B:/~BUN/root/runwork-windows-x64.exe',
76
+ 'dev',
77
+ '--detach',
78
+ ];
79
+ expect(buildChildArgs(argv)).toEqual([
80
+ 'dev', '--detach', INTERNAL_DETACHED_CHILD_FLAG,
81
+ ]);
82
+ });
83
+ it('strips an existing internal-child flag and re-adds exactly one', () => {
84
+ const argv = [
85
+ '/usr/local/bin/runwork',
86
+ 'dev',
87
+ '--detach',
88
+ INTERNAL_DETACHED_CHILD_FLAG,
89
+ INTERNAL_DETACHED_CHILD_FLAG,
90
+ ];
91
+ const out = buildChildArgs(argv);
92
+ expect(out.filter((a) => a === INTERNAL_DETACHED_CHILD_FLAG)).toHaveLength(1);
93
+ });
94
+ it('preserves the order of user args and only mutates argv[0..1]', () => {
95
+ const argv = [
96
+ 'C:\\bin\\runwork.exe',
97
+ 'B:/~BUN/root/runwork-windows-x64.exe',
98
+ 'dev',
99
+ '--restart',
100
+ '--detach',
101
+ '--json',
102
+ ];
103
+ expect(buildChildArgs(argv)).toEqual([
104
+ 'dev', '--restart', '--detach', '--json', INTERNAL_DETACHED_CHILD_FLAG,
105
+ ]);
106
+ });
107
+ it('handles an empty argv tail (just the binary)', () => {
108
+ expect(buildChildArgs(['runwork'])).toEqual([INTERNAL_DETACHED_CHILD_FLAG]);
109
+ });
110
+ });
35
111
  describe('pollForSession()', () => {
36
112
  let appDir;
37
113
  beforeEach(() => {
@@ -162,3 +162,26 @@ export declare function isInternalDetachedChild(argv?: readonly string[]): boole
162
162
  * misconfigured wrapper).
163
163
  */
164
164
  export declare function stripInternalChildFlag(args: readonly string[]): string[];
165
+ /**
166
+ * Detect a Bun standalone virtual-filesystem path. Bun's compile mode
167
+ * on Windows injects the in-bundle script path as `process.argv[1]`
168
+ * (e.g., `B:/~BUN/root/runwork-windows-x64.exe`). When we self-spawn,
169
+ * the child Bun runtime re-injects an equivalent entry on its own --
170
+ * forwarding ours causes a duplicate that downstream parsers (commander
171
+ * here) misread as a stray positional command. macOS and Linux Bun
172
+ * standalone do NOT inject this entry, but the prefix is documented in
173
+ * Bun source as `/$bunfs/` if it ever appears, so we detect that too
174
+ * defensively.
175
+ */
176
+ export declare function looksLikeBunStandaloneArtifact(p: string): boolean;
177
+ /**
178
+ * Construct the args we should forward to the spawned child so it
179
+ * re-runs the same `runwork dev` invocation as the parent. Drops
180
+ * elements that the child runtime will re-inject on its own (notably
181
+ * the Bun-on-Windows virtual-FS path) and drops any pre-existing copy
182
+ * of the internal-child marker before we re-add exactly one.
183
+ *
184
+ * Pure function for testability -- accepts the parent's argv and returns
185
+ * what to hand to `spawn`. Real callers pass `process.argv`.
186
+ */
187
+ export declare function buildChildArgs(parentArgv: readonly string[]): string[];
@@ -245,3 +245,48 @@ export function isInternalDetachedChild(argv = process.argv) {
245
245
  export function stripInternalChildFlag(args) {
246
246
  return args.filter((a) => a !== INTERNAL_DETACHED_CHILD_FLAG);
247
247
  }
248
+ /**
249
+ * Detect a Bun standalone virtual-filesystem path. Bun's compile mode
250
+ * on Windows injects the in-bundle script path as `process.argv[1]`
251
+ * (e.g., `B:/~BUN/root/runwork-windows-x64.exe`). When we self-spawn,
252
+ * the child Bun runtime re-injects an equivalent entry on its own --
253
+ * forwarding ours causes a duplicate that downstream parsers (commander
254
+ * here) misread as a stray positional command. macOS and Linux Bun
255
+ * standalone do NOT inject this entry, but the prefix is documented in
256
+ * Bun source as `/$bunfs/` if it ever appears, so we detect that too
257
+ * defensively.
258
+ */
259
+ export function looksLikeBunStandaloneArtifact(p) {
260
+ // Windows: drive-letter paths under \~BUN\, e.g. "B:/~BUN/root/..." or "B:\~BUN\root\..."
261
+ if (/^[a-z]:[/\\]~BUN[/\\]/i.test(p))
262
+ return true;
263
+ // Linux/macOS bunfs prefix (documented; not currently emitted in user-visible argv).
264
+ if (p.startsWith('/$bunfs/'))
265
+ return true;
266
+ return false;
267
+ }
268
+ /**
269
+ * Construct the args we should forward to the spawned child so it
270
+ * re-runs the same `runwork dev` invocation as the parent. Drops
271
+ * elements that the child runtime will re-inject on its own (notably
272
+ * the Bun-on-Windows virtual-FS path) and drops any pre-existing copy
273
+ * of the internal-child marker before we re-add exactly one.
274
+ *
275
+ * Pure function for testability -- accepts the parent's argv and returns
276
+ * what to hand to `spawn`. Real callers pass `process.argv`.
277
+ */
278
+ export function buildChildArgs(parentArgv) {
279
+ // argv[0] is always the binary; the child gets it back via spawn's
280
+ // execPath argument. We start scanning from argv[1].
281
+ const rest = [];
282
+ for (let i = 1; i < parentArgv.length; i++) {
283
+ if (i === 1 && looksLikeBunStandaloneArtifact(parentArgv[i])) {
284
+ // Skip Bun-on-Windows's auto-injected virtual-FS path; the child
285
+ // runtime injects its own equivalent entry. Forwarding ours would
286
+ // duplicate it in the child's argv.
287
+ continue;
288
+ }
289
+ rest.push(parentArgv[i]);
290
+ }
291
+ return [...stripInternalChildFlag(rest), INTERNAL_DETACHED_CHILD_FLAG];
292
+ }
@@ -1 +1 @@
1
- export declare const VERSION = "0.10.3";
1
+ export declare const VERSION = "0.11.0";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-types.ts -- do not edit
2
- export const VERSION = "0.10.3";
2
+ export const VERSION = "0.11.0";
@@ -60,7 +60,7 @@ describe('git/credentials', () => {
60
60
  '--global',
61
61
  '--unset',
62
62
  'credential.https://runwork.ai.helper',
63
- ], { stdio: 'pipe' });
63
+ ], { stdio: 'pipe', windowsHide: true });
64
64
  });
65
65
  it('ignores errors silently', async () => {
66
66
  mockExecFileSync.mockImplementation(() => {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { readFileSync } from 'fs';
3
3
  import { watch } from 'chokidar';
4
4
  import { join, relative } from 'path';
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { getCredentials } from '../auth/store.js';
3
3
  /**
4
4
  * Build the value we hand to `git config credential.<origin>.helper`.
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  /**
3
3
  * Default identity used when we cannot derive anything better from the
4
4
  * authenticated credentials. Local-scoped, never written to global config,
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { existsSync } from 'fs';
3
3
  import { win32 as winPath } from 'path';
4
4
  import { homedir } from 'os';
package/dist/git/sync.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { unlinkSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  export function hasCommits(cwd) {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync } from '../utils/subprocess.js';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { join, sep } from 'path';
4
4
  import { homedir, platform as osPlatform, arch as osArch } from 'os';
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'crypto';
2
- import { execFileSync } from 'child_process';
2
+ import { execFileSync } from '../utils/subprocess.js';
3
3
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import { join, relative } from 'path';
5
5
  import { buildIgnoreSets } from '../utils/ignore-matcher.js';
package/dist/types.d.ts CHANGED
@@ -121,6 +121,25 @@ export interface McpServerConfig {
121
121
  lastConnectedAt?: number;
122
122
  toolCount?: number;
123
123
  }
124
+ /**
125
+ * Per-agent state tracking baked-in default permission rules that the CLI
126
+ * injects automatically during sync, with sticky opt-out semantics.
127
+ *
128
+ * lastInjected: what we wrote on the previous sync (used to detect when the
129
+ * user removed an entry from the agent's config file).
130
+ * userOptOuts: ever-accumulating set of entries the user has removed at least
131
+ * once. Never re-injected until the tool is removed entirely.
132
+ */
133
+ export interface AgentDefaultsState {
134
+ lastInjected: {
135
+ allow?: string[];
136
+ deny?: string[];
137
+ };
138
+ userOptOuts: {
139
+ allow?: string[];
140
+ deny?: string[];
141
+ };
142
+ }
124
143
  export interface SetupState {
125
144
  workspaceId: string;
126
145
  workspaceName: string;
@@ -147,6 +166,10 @@ export interface SetupState {
147
166
  * on every cycle while still catching newly-installed agents within a day.
148
167
  */
149
168
  lastDetectedAt?: string;
169
+ /** Schema version for agentDefaults; absence means bootstrap on next sync. */
170
+ agentDefaultsVersion?: number;
171
+ /** Per-agent slug => default rule state. Absence of an entry means bootstrap. */
172
+ agentDefaults?: Record<string, AgentDefaultsState>;
150
173
  }
151
174
  export interface WorkflowInfo {
152
175
  name: string;
package/dist/ui/banner.js CHANGED
@@ -10,9 +10,9 @@ function prettyPath(dir) {
10
10
  return dir;
11
11
  }
12
12
  export const SUPPORTED_AGENTS = [
13
+ { id: 'codex', name: 'OpenAI Codex', command: 'cd {dir} && codex' },
13
14
  { id: 'claude-code', name: 'Claude Code', command: 'cd {dir} && claude' },
14
15
  { id: 'cursor', name: 'Cursor', command: 'cursor {dir}' },
15
- { id: 'codex', name: 'OpenAI Codex', command: 'cd {dir} && codex' },
16
16
  { id: 'claude-desktop', name: 'Claude Desktop', command: 'Open Claude Desktop and add {dir} as a project folder' },
17
17
  { id: 'antigravity', name: 'Antigravity', command: 'cd {dir} && antigravity' },
18
18
  ];
@@ -24,6 +24,19 @@ export interface AgentResponse<T> {
24
24
  }
25
25
  /** Map of key template files and what they're for. */
26
26
  export declare const APP_STRUCTURE: Record<string, string>;
27
+ /**
28
+ * The single most important workflow rule for AI agents working on a
29
+ * Runwork app. Repeated across multiple guides because it's the
30
+ * difference between a working dev cycle and a frustrating one.
31
+ *
32
+ * Rationale -- agents that don't follow this end up:
33
+ * 1. Editing files locally with no preview to verify against.
34
+ * 2. Hitting a single bulk-sync at dev startup that masks which
35
+ * change broke what (instead of incremental per-change syncs).
36
+ * 3. Risking sync conflicts when the local state has diverged from
37
+ * the remote in a way the auto-sync can't reconcile.
38
+ */
39
+ export declare const DEV_FIRST_RULE = "CRITICAL: Always start `runwork dev --detach` BEFORE editing any code in this app. Dev establishes the live sync pipeline -- without it, changes do not flow to the preview, and the dev startup will batch-sync everything at once instead of giving you per-change feedback. To check whether dev is already running for this app, run `runwork info --json` and inspect `localDevSession.state` (alive / stale / none). If alive, just edit. If none or stale, run `runwork dev --detach --json` first, capture the preview URL from the `session_started` event, then proceed with edits.";
27
40
  export declare function buildInitGuide(appName: string, dir: string): AgentGuide;
28
41
  export declare function buildCloneGuide(appName: string, dir: string): AgentGuide;
29
42
  export declare function buildDevSessionGuide(): AgentGuide;
@@ -18,7 +18,21 @@ export const APP_STRUCTURE = {
18
18
  'blueprint.json': 'App feature registry. Update after adding entities, workflows, agents, etc.',
19
19
  'CLAUDE.md': 'Complete framework documentation. Read this before editing anything.',
20
20
  };
21
+ /**
22
+ * The single most important workflow rule for AI agents working on a
23
+ * Runwork app. Repeated across multiple guides because it's the
24
+ * difference between a working dev cycle and a frustrating one.
25
+ *
26
+ * Rationale -- agents that don't follow this end up:
27
+ * 1. Editing files locally with no preview to verify against.
28
+ * 2. Hitting a single bulk-sync at dev startup that masks which
29
+ * change broke what (instead of incremental per-change syncs).
30
+ * 3. Risking sync conflicts when the local state has diverged from
31
+ * the remote in a way the auto-sync can't reconcile.
32
+ */
33
+ export const DEV_FIRST_RULE = 'CRITICAL: Always start `runwork dev --detach` BEFORE editing any code in this app. Dev establishes the live sync pipeline -- without it, changes do not flow to the preview, and the dev startup will batch-sync everything at once instead of giving you per-change feedback. To check whether dev is already running for this app, run `runwork info --json` and inspect `localDevSession.state` (alive / stale / none). If alive, just edit. If none or stale, run `runwork dev --detach --json` first, capture the preview URL from the `session_started` event, then proceed with edits.';
21
34
  const COMMON_TIPS = [
35
+ DEV_FIRST_RULE,
22
36
  'Read CLAUDE.md in the app directory first -- it has complete framework documentation with code examples.',
23
37
  'You do NOT need to run git commands manually. runwork dev handles file syncing automatically. (git itself must be installed on the system -- see dependencies.)',
24
38
  'Do NOT install external AI SDKs (openai, @anthropic-ai/sdk). Use @runworkai/framework/ai instead.',
@@ -39,8 +53,8 @@ export function buildInitGuide(appName, dir) {
39
53
  structure: APP_STRUCTURE,
40
54
  nextSteps: [
41
55
  `cd ${dir}`,
42
- 'runwork dev # start development server -- watches for file changes and syncs automatically',
43
- 'Edit files for your needs (see structure above)',
56
+ 'runwork dev --detach --json # FIRST: start the dev sandbox in the background; capture the preview URL from the session_started event',
57
+ 'THEN edit files for your needs (see structure above) -- changes auto-sync to the preview',
44
58
  'runwork deploy # deploy to production when ready',
45
59
  ],
46
60
  tips: COMMON_TIPS,
@@ -53,9 +67,9 @@ export function buildCloneGuide(appName, dir) {
53
67
  structure: APP_STRUCTURE,
54
68
  nextSteps: [
55
69
  `cd ${dir}`,
56
- 'runwork dev # start development server',
70
+ 'runwork dev --detach --json # FIRST: start dev in the background, capture the preview URL',
57
71
  'Review existing files to understand what is already built',
58
- 'Edit files for your needs',
72
+ 'THEN edit files for your needs -- changes auto-sync to the preview',
59
73
  'runwork deploy # deploy to production when ready',
60
74
  ],
61
75
  tips: [SYSTEM_DEPENDENCIES_NOTE, ...COMMON_TIPS],
@@ -89,17 +103,18 @@ export function buildDeployGuide() {
89
103
  }
90
104
  export function buildInfoGuide() {
91
105
  return {
92
- context: 'This shows the current state of the app: what is deployed, what integrations are connected, and what resources (entities, workflows, agents, etc.) are registered.',
106
+ context: 'This shows the current state of the app: what is deployed, what integrations are connected, and what resources (entities, workflows, agents, etc.) are registered. The `localDevSession` field tells you whether a dev session is already running on this machine -- check it BEFORE starting a new one.',
93
107
  nextSteps: [
94
- 'runwork dev # start development server if not already running',
108
+ 'If localDevSession.state == "alive": dev is already running. Use the URL in localDevSession.previewUrl and proceed with edits.',
109
+ 'If localDevSession.state == "none" or "stale": run `runwork dev --detach --json` BEFORE editing any code. Capture the preview URL from the session_started event.',
95
110
  'runwork deploy # deploy to production when ready',
96
111
  ],
97
112
  tips: [
113
+ DEV_FIRST_RULE,
98
114
  'Entities listed here are the data models available via Entity CRUD methods.',
99
115
  'Workflows listed here can be triggered via their registered endpoints or schedules.',
100
116
  'Agents listed here need frontend pages (conversational) or triggers (task) to be accessible.',
101
117
  'Integrations with connected status are ready to use. Others need setup in workspace settings.',
102
- 'If preview is not active, run runwork dev to start a development session.',
103
118
  ],
104
119
  };
105
120
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Thin wrappers around `child_process.execFileSync` and `spawn` that
3
+ * default `windowsHide: true`. On Windows, every console-subsystem
4
+ * subprocess we spawn (git, where.exe, registry queries) creates its
5
+ * own console window unless this flag is set. With our hot paths --
6
+ * auto-commit's git invocations, manifest's `git ls-files`, the sync
7
+ * loop's git rebase / fetch / push -- a missing flag visibly flashes a
8
+ * console window every few seconds, which Codex Desktop users see as
9
+ * "empty terminals keep popping up."
10
+ *
11
+ * Use these as drop-in replacements for the `child_process` exports.
12
+ * Existing options the caller passes still win (so you can opt back to
13
+ * `windowsHide: false` if you genuinely need the window, e.g. for
14
+ * interactive prompts -- though we don't have any of those in our
15
+ * subprocess paths today).
16
+ */
17
+ import { execFileSync as cpExecFileSync, spawn as cpSpawn } from 'child_process';
18
+ export declare const execFileSync: typeof cpExecFileSync;
19
+ export declare const spawn: typeof cpSpawn;