@xdelivered/emberflow 0.2.0 → 0.5.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 (116) hide show
  1. package/bin/commands.ts +7 -3
  2. package/bin/init.test.ts +94 -0
  3. package/bin/init.ts +108 -2
  4. package/dist/bin/commands.d.ts +1 -0
  5. package/dist/bin/commands.js +6 -3
  6. package/dist/bin/init.d.ts +4 -0
  7. package/dist/bin/init.js +96 -2
  8. package/dist/server/agents/codexParse.js +31 -0
  9. package/dist/server/agents/detect.d.ts +18 -8
  10. package/dist/server/agents/detect.js +78 -13
  11. package/dist/server/agents/modelRejectionHint.js +1 -1
  12. package/dist/server/agents/prompt.d.ts +15 -1
  13. package/dist/server/agents/prompt.js +59 -37
  14. package/dist/server/agents/runManager.d.ts +23 -2
  15. package/dist/server/agents/runManager.js +36 -8
  16. package/dist/server/client.d.ts +1 -0
  17. package/dist/server/client.js +7 -2
  18. package/dist/server/index.js +208 -16
  19. package/dist/server/nodesPayload.d.ts +14 -1
  20. package/dist/server/nodesPayload.js +15 -2
  21. package/dist/server/projectMode.js +5 -2
  22. package/dist/server/runRegistry.d.ts +52 -1
  23. package/dist/server/runRegistry.js +170 -1
  24. package/dist/server/sourceNav.d.ts +77 -0
  25. package/dist/server/sourceNav.js +503 -0
  26. package/dist/server/subflowRunner.d.ts +48 -1
  27. package/dist/server/subflowRunner.js +34 -7
  28. package/dist/server/updateCheck.d.ts +68 -0
  29. package/dist/server/updateCheck.js +142 -0
  30. package/dist/src/engine/executor.js +4 -3
  31. package/dist/src/engine/registry.d.ts +27 -2
  32. package/dist/src/engine/registry.js +84 -2
  33. package/dist/src/nodes/index.d.ts +8 -2
  34. package/dist/src/nodes/index.js +7 -3
  35. package/dist/src/nodes/login.d.ts +3 -1
  36. package/dist/src/nodes/login.js +2 -2
  37. package/package.json +28 -7
  38. package/server/agentRoute.test.ts +20 -0
  39. package/server/agents/codexParse.test.ts +37 -0
  40. package/server/agents/codexParse.ts +34 -0
  41. package/server/agents/detect.test.ts +26 -7
  42. package/server/agents/detect.ts +82 -14
  43. package/server/agents/modelRejectionHint.ts +2 -1
  44. package/server/agents/prompt.test.ts +128 -0
  45. package/server/agents/prompt.ts +102 -3
  46. package/server/agents/runManager.test.ts +9 -0
  47. package/server/agents/runManager.ts +37 -7
  48. package/server/client.ts +10 -4
  49. package/server/index.ts +213 -15
  50. package/server/nodeRun.test.ts +189 -0
  51. package/server/nodesPayload.test.ts +38 -0
  52. package/server/nodesPayload.ts +28 -3
  53. package/server/projectMode.ts +5 -2
  54. package/server/runRegistry.ts +200 -3
  55. package/server/setupStatus.test.ts +3 -0
  56. package/server/sourceNav.test.ts +382 -0
  57. package/server/sourceNav.ts +644 -0
  58. package/server/sourceNavRoute.test.ts +163 -0
  59. package/server/stepDrill.test.ts +380 -0
  60. package/server/subflowRunner.ts +92 -11
  61. package/server/updateCheck.test.ts +209 -0
  62. package/server/updateCheck.ts +175 -0
  63. package/server/workflowTestRoute.test.ts +1 -1
  64. package/src/App.tsx +82 -2
  65. package/src/components/AgentConsole.tsx +7 -280
  66. package/src/components/AgentStream.test.tsx +88 -0
  67. package/src/components/AgentStream.tsx +303 -0
  68. package/src/components/CreateModal.tsx +43 -9
  69. package/src/components/Dock.tsx +1 -1
  70. package/src/components/EmptyState.test.tsx +49 -0
  71. package/src/components/EmptyState.tsx +61 -0
  72. package/src/components/EnvironmentDialog.tsx +4 -1
  73. package/src/components/EnvironmentPicker.tsx +8 -3
  74. package/src/components/EnvironmentsDialog.tsx +4 -1
  75. package/src/components/InfraPanel.tsx +30 -2
  76. package/src/components/InfrastructureDialog.test.tsx +97 -0
  77. package/src/components/InfrastructureDialog.tsx +111 -0
  78. package/src/components/Inspector.tsx +9 -26
  79. package/src/components/RunbookView.tsx +70 -6
  80. package/src/components/SettingsDialog.tsx +4 -59
  81. package/src/components/Sidebar.tsx +6 -26
  82. package/src/components/SourceNavigator.test.tsx +226 -0
  83. package/src/components/SourceNavigator.tsx +520 -0
  84. package/src/components/StatusBar.tsx +227 -26
  85. package/src/components/Toolbar.tsx +28 -16
  86. package/src/components/UpdateChip.test.tsx +31 -0
  87. package/src/components/WelcomeDialog.test.tsx +384 -13
  88. package/src/components/WelcomeDialog.tsx +996 -177
  89. package/src/engine/executor.ts +4 -3
  90. package/src/engine/registry.sourceRef.test.ts +112 -0
  91. package/src/engine/registry.ts +103 -2
  92. package/src/lib/guidedQuestions.test.ts +224 -0
  93. package/src/lib/guidedQuestions.ts +181 -0
  94. package/src/lib/runbookModel.ts +3 -3
  95. package/src/lib/runbookProjection.test.ts +43 -0
  96. package/src/lib/runbookProjection.ts +26 -6
  97. package/src/nodes/index.ts +10 -3
  98. package/src/nodes/login.ts +5 -2
  99. package/src/store/agentClient.ts +27 -8
  100. package/src/store/apiTree.ts +12 -0
  101. package/src/store/builderStore.test.ts +441 -99
  102. package/src/store/builderStore.ts +385 -314
  103. package/src/store/nodeMeta.ts +10 -2
  104. package/src/store/serverRunner.ts +56 -5
  105. package/src/store/setupClient.ts +7 -2
  106. package/src/store/sourceNavClient.ts +48 -0
  107. package/src/store/updateClient.ts +50 -0
  108. package/studio-dist/assets/index-BbzppDpt.js +65 -0
  109. package/studio-dist/assets/index-CLf6blcA.css +1 -0
  110. package/studio-dist/index.html +2 -2
  111. package/templates/skills/emberflow-basics/SKILL.md +29 -1
  112. package/templates/skills/emberflow-model-process/SKILL.md +92 -9
  113. package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
  114. package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
  115. package/studio-dist/assets/index-DNJwW-hM.css +0 -1
  116. package/studio-dist/assets/index-O26dKRjW.js +0 -65
@@ -1,11 +1,29 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- const CANDIDATES = ['codex', 'claude'];
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
3
4
  const VERSION_RE = /\d+\.\d+\.\d+/;
4
5
  /**
5
- * Real PATH probe: runs `<bin> --version` and parses the first semver-ish
6
- * token out of stdout. Returns `undefined` when the binary isn't present/
7
- * runnable (spawn error, timeout, or nonzero exit); `null` version when it
8
- * ran but produced no parseable version string.
6
+ * Where each agent CLI may live, beyond whatever PATH resolves. PATH shims can
7
+ * pin an OLD version (e.g. a superset-managed ~/.superset/bin/codex frozen at
8
+ * a release the API no longer accepts) while a newer binary sits elsewhere —
9
+ * so we probe every known location and pick the NEWEST, never just the first.
10
+ */
11
+ const CANDIDATE_BINS = {
12
+ codex: [
13
+ 'codex', // PATH resolution (may be a pinned shim)
14
+ join(homedir(), '.local', 'bin', 'codex'),
15
+ '/opt/homebrew/bin/codex',
16
+ '/usr/local/bin/codex',
17
+ // The ChatGPT desktop app bundles its own codex, updated with the app.
18
+ '/Applications/ChatGPT.app/Contents/Resources/codex',
19
+ ],
20
+ claude: ['claude', join(homedir(), '.local', 'bin', 'claude'), '/opt/homebrew/bin/claude', '/usr/local/bin/claude'],
21
+ };
22
+ /**
23
+ * Real probe: runs `<bin> --version` and parses the first semver-ish token out
24
+ * of stdout. Returns `undefined` when the binary isn't present/runnable (spawn
25
+ * error, timeout, or nonzero exit); `null` version when it ran but produced no
26
+ * parseable version string.
9
27
  */
10
28
  export function probe(bin) {
11
29
  const result = spawnSync(bin, ['--version'], {
@@ -18,18 +36,65 @@ export function probe(bin) {
18
36
  const match = result.stdout?.match(VERSION_RE);
19
37
  return { version: match ? match[0] : null };
20
38
  }
39
+ /** Numeric segment compare; a parseable version always beats null.
40
+ * Shared: agent-CLI detection here, and the package update check
41
+ * (server/updateCheck.ts) both rank semver-ish strings with it. */
42
+ export function newer(a, b) {
43
+ if (a === null)
44
+ return false;
45
+ if (b === null)
46
+ return true;
47
+ const as = a.split('.').map(Number);
48
+ const bs = b.split('.').map(Number);
49
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
50
+ const d = (as[i] ?? 0) - (bs[i] ?? 0);
51
+ if (d !== 0)
52
+ return d > 0;
53
+ }
54
+ return false;
55
+ }
21
56
  /**
22
- * Detects which coding-agent CLIs are available on PATH, and the version
23
- * each reports (null when present but unparseable). `probeBin` is
24
- * injectable so tests can stub PATH presence/version without shelling out
25
- * for real binaries.
57
+ * Detects which coding-agent CLIs are available probing PATH plus the known
58
+ * install locations for each kind and keeps the NEWEST version found per
59
+ * kind, with the binary path it came from. `probeBin` is injectable so tests
60
+ * can stub presence/version per location without shelling out.
26
61
  */
27
62
  export function detectAgents(probeBin = probe) {
28
63
  const found = [];
29
- for (const kind of CANDIDATES) {
30
- const result = probeBin(kind);
31
- if (result)
32
- found.push({ kind, version: result.version });
64
+ for (const kind of Object.keys(CANDIDATE_BINS)) {
65
+ let best;
66
+ const seen = new Set();
67
+ for (const bin of CANDIDATE_BINS[kind]) {
68
+ if (seen.has(bin))
69
+ continue;
70
+ seen.add(bin);
71
+ const result = probeBin(bin);
72
+ if (!result)
73
+ continue;
74
+ if (!best || newer(result.version, best.version)) {
75
+ best = { kind, version: result.version, bin };
76
+ }
77
+ }
78
+ if (best)
79
+ found.push(best);
33
80
  }
34
81
  return found;
35
82
  }
83
+ // Probing every candidate spawns several subprocesses; /setup-status and
84
+ // /agent/available are polled by the studio, so cache briefly.
85
+ const RESOLVE_TTL_MS = 30_000;
86
+ let cachedAt = 0;
87
+ let cached = null;
88
+ /** Cached detection — the newest binary per kind, refreshed every 30s. */
89
+ export function detectAgentsCached() {
90
+ const now = Date.now();
91
+ if (!cached || now - cachedAt > RESOLVE_TTL_MS) {
92
+ cached = detectAgents();
93
+ cachedAt = now;
94
+ }
95
+ return cached;
96
+ }
97
+ /** The concrete binary path spawns should use for `kind` — the newest found. */
98
+ export function resolveAgentBin(kind) {
99
+ return detectAgentsCached().find((a) => a.kind === kind)?.bin;
100
+ }
@@ -1,4 +1,4 @@
1
- const MODEL_REJECTION_RE = /unknown model|unsupported model|invalid model|model.*not.*(found|supported)/i;
1
+ const MODEL_REJECTION_RE = /unknown model|unsupported model|invalid model|model.*not.*(found|supported)|model requires a newer version/i;
2
2
  /**
3
3
  * When an agent CLI exits nonzero without ever emitting a terminal `done`,
4
4
  * inspect its buffered stderr tail for known model-rejection shapes (a stale
@@ -27,6 +27,9 @@ export type AgentIntent = {
27
27
  } | {
28
28
  action: 'scout-infrastructure';
29
29
  instruction: string;
30
+ } | {
31
+ action: 'guided-setup';
32
+ instruction: string;
30
33
  } | {
31
34
  action: 'cover-operation';
32
35
  flowId: string;
@@ -76,4 +79,15 @@ export interface AvailableNode {
76
79
  * instead of inventing parallel config. The `scout-infrastructure` intent
77
80
  * itself is what WRITES this manifest, so it does not receive the block.
78
81
  */
79
- export declare function buildPrompt(intent: AgentIntent, apisDir: string, relPath: string, availableNodes?: AvailableNode[], projectLanguage?: 'javascript' | 'typescript', infrastructure?: InfrastructureManifest | null): string;
82
+ /** Ground truth the RUNNER already verified injected into the guided-setup
83
+ * prompt so the agent starts from known facts instead of burning its first
84
+ * turn re-deriving them with CLI probes and file reads. */
85
+ export interface GuidedSetupState {
86
+ gitRepo: boolean;
87
+ skillsInstalled: boolean;
88
+ environmentsConfigured: boolean;
89
+ infrastructurePresent: boolean;
90
+ opCount: number;
91
+ onlyHello: boolean;
92
+ }
93
+ export declare function buildPrompt(intent: AgentIntent, apisDir: string, relPath: string, availableNodes?: AvailableNode[], projectLanguage?: 'javascript' | 'typescript', infrastructure?: InfrastructureManifest | null, guidedState?: GuidedSetupState | null): string;
@@ -1,45 +1,21 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { dirname, join, resolve } from 'node:path';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  /** Absolute path to the register-API CLI bin (`node <this> <cmd>`), resolved
4
5
  * from this file's location. This exact invocation is the ONLY sandbox-safe
5
6
  * way to run the CLI: `npx emberflow` / `tsx` spawn a tsx IPC pipe the codex
6
- * sandbox blocks; this bin runs the CLI in-process under tsx's register API. */
7
- const EMBERFLOW_BIN = resolve(dirname(fileURLToPath(import.meta.url)), '../../bin/emberflow.mjs');
8
- /**
9
- * Builds a natural-language prompt for a coding agent (Codex/Claude) that
10
- * turns a studio `AgentIntent` into skill-aware instructions for editing
11
- * Emberflow flow files. Pure and deterministic: same inputs always produce
12
- * the same prompt string.
7
+ * sandbox blocks; this bin runs the CLI in-process under tsx's register API.
13
8
  *
14
- * `relPath` is the operation's on-disk relative path under `apisDir` (e.g.
15
- * `default/triage`) NOT necessarily the same as `intent.flowId` (the
16
- * in-file `id`), since operations are keyed by id but stored by path. For
17
- * `new-operation` there is no existing flow yet, so `relPath` instead carries
18
- * the *resolved target directory* under `apisDir` (e.g. `billing` or
19
- * `default`) that the new operation file should be created inside.
20
- *
21
- * `availableNodes` is the full, consumer-specific set of registered node
22
- * types (domain nodes like `ValidateCredentials`/`FetchUser` plus built-ins
23
- * like `Input`/`Result`/`Response`/`requireAuth`). It is injected so the agent
24
- * knows what it can REUSE — but nodes are real code it can also author. The
25
- * prompt encourages inventing a node (registering its implementation) when no
26
- * existing one fits; the single guardrail is that a referenced `type` must be
27
- * registered in the same change, so a made-up type with no implementation
28
- * (e.g. a bare `CurrentServerTime`) can't slip through validation.
29
- *
30
- * `projectLanguage` is the target project's authored language (from its
31
- * `ProjectConfig.language`, explicit-or-inferred). Defaults to 'typescript' —
32
- * the Emberflow repo itself, when this function is called without a loaded
33
- * consumer project.
34
- *
35
- * `infrastructure` is the loaded `emberflow/infrastructure.json` manifest (or
36
- * `null` when the project hasn't been scouted / the file is malformed). When
37
- * present, a preamble block after the node palette lists what the project
38
- * already uses so the agent REUSES it (same secretRef names, same systems)
39
- * instead of inventing parallel config. The `scout-infrastructure` intent
40
- * itself is what WRITES this manifest, so it does not receive the block.
41
- */
42
- export function buildPrompt(intent, apisDir, relPath, availableNodes = [], projectLanguage = 'typescript', infrastructure = null) {
9
+ * Two layouts: in the source repo this file is server/agents/prompt.ts and
10
+ * bin/ is two levels up; in the shipped package it runs from
11
+ * dist/server/agents/prompt.js while bin/ stays at the package ROOT three
12
+ * levels up. Probe both so consumer installs don't hand agents a dead path. */
13
+ const EMBERFLOW_BIN = (() => {
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const candidates = [resolve(here, '../../bin/emberflow.mjs'), resolve(here, '../../../bin/emberflow.mjs')];
16
+ return candidates.find((p) => existsSync(p)) ?? candidates[0];
17
+ })();
18
+ export function buildPrompt(intent, apisDir, relPath, availableNodes = [], projectLanguage = 'typescript', infrastructure = null, guidedState = null) {
43
19
  const lines = [];
44
20
  /** Shared invariant for every flow-mutating intent: doctor is the mechanical
45
21
  * checker (missing param defaults, uncovered path params, missing expects),
@@ -80,7 +56,7 @@ export function buildPrompt(intent, apisDir, relPath, availableNodes = [], proje
80
56
  // REUSES it instead of inventing parallel config. Omitted for the scout
81
57
  // intent — it's the one that WRITES the manifest, so priming it with the
82
58
  // manifest's own contents would be circular.
83
- if (intent.action !== 'scout-infrastructure') {
59
+ if (intent.action !== 'scout-infrastructure' && intent.action !== 'guided-setup') {
84
60
  const INFRASTRUCTURE_ITEM_CAP = 30;
85
61
  if (infrastructure && infrastructure.items.length > 0) {
86
62
  lines.push('Known project infrastructure (from emberflow/infrastructure.json — what this project already uses):');
@@ -291,6 +267,8 @@ export function buildPrompt(intent, apisDir, relPath, availableNodes = [], proje
291
267
  lines.push(`User instruction (verbatim): ${intent.instruction}`);
292
268
  lines.push('');
293
269
  }
270
+ lines.push(`AMENDMENT vs FULL RESCAN: if the instruction above asks for an amendment to the existing manifest — add, remove, or correct SPECIFIC items — and emberflow/infrastructure.json already exists, UPDATE that file in place: preserve every item you were not asked to change, apply only the requested change, and bump "scannedAt" to the current timestamp. Do NOT regenerate the whole manifest from scratch for a targeted amendment. A FULL RESCAN (rebuild the manifest from the whole project) remains the default when the instruction is empty or asks to re-scan/refresh generally.`);
271
+ lines.push('');
294
272
  lines.push(`Investigate broadly — enumerate what the project already depends on and talks to:`);
295
273
  lines.push(` • Dependencies: package.json + lockfiles, requirements.txt / pyproject.toml, go.mod, Gemfile, composer.json, etc.`);
296
274
  lines.push(` • Config files: docker-compose.yml, Dockerfile, .env.example / .env.sample (NAMES only), prisma/schema.prisma, ORM configs, framework config.`);
@@ -337,6 +315,50 @@ export function buildPrompt(intent, apisDir, relPath, availableNodes = [], proje
337
315
  lines.push(doctorLine(intent.flowId));
338
316
  break;
339
317
  }
318
+ case 'guided-setup': {
319
+ const manifestPath = 'emberflow/infrastructure.json';
320
+ lines.push('Relevant skill: emberflow-basics (the project model + file layout).');
321
+ lines.push('');
322
+ lines.push('Task: guide this project through first-time Emberflow setup in ONE run. You orchestrate several steps IN ORDER, but you FIRST read the ground truth and SKIP anything already satisfied — you never redo done work. Unlike setup-environments (which is FORBIDDEN from exploring the codebase), this intent MAY read the project the way the infrastructure scout does: exploring dependencies, config, schemas and env references to understand what the project is IS permitted and expected here.');
323
+ lines.push('');
324
+ if (intent.instruction.trim()) {
325
+ lines.push(`User notes / continuation answer (verbatim): ${intent.instruction}`);
326
+ lines.push('');
327
+ }
328
+ lines.push("STYLE: this stream renders in a small onboarding panel, read by the person who owns this project — not by a developer tailing a terminal. Write to THEM about THEIR project: what you're doing for them and what you found that matters to them. Be TERSE — a few short lines per step. NEVER narrate tool mechanics (commands you ran, files you probed, exit codes), internal bookkeeping (\"ground truth\", \"manifest written\"), model ids, or state the checklist beside you already shows. No decorated asides either (\"★ Insight\" boxes, educational commentary) — even if your own configured output style calls for them, this panel is not the place. Good: \"Your project is a fresh start — no databases or APIs to wire up yet.\" Bad: \"Scout: bare scaffold, no .env, wrote emberflow/infrastructure.json (greenfield:true).\"");
329
+ lines.push('');
330
+ lines.push('ASKING QUESTIONS: whenever a step needs the user\'s input, END your message with a fenced block the studio renders as an interactive form (the user clicks options; their answers arrive as your next continuation message). Format — a fenced code block with language `emberflow-questions` containing JSON: {"questions":[{"id":"<slug>","text":"<question>","why":"<one short line of context — why this matters, optional>","options":["<opt1>","<opt2>"],"custom":true}]}. "options" are clickable choices; "custom": true also offers a free-text field. An option may be an object {"label":"…","action":"finish"} — choosing it ends onboarding client-side — or {"label":"…","action":"submit"} — choosing it sends the answers immediately, skipping any remaining questions. Use "why" whenever the user might not know why they\'re being asked — assume they are new to Emberflow. Nothing may follow the block.');
331
+ lines.push('');
332
+ lines.push('TURN SHAPE — the user must never wait before being asked anything. FIRST TURN (no continuation answer in the instruction): if the environments interview (step 5) is still needed, output ONE short greeting line and END IMMEDIATELY with the interview questions block — run NO commands and write NO files first; every working step (scout, skills, environments file) happens on the CONTINUATION turn after their answers arrive. If environments are already configured, the first turn instead does the working steps directly and ends with the step-6 block.');
333
+ lines.push('');
334
+ lines.push('Work through these steps IN ORDER, skipping any that your ground-truth read shows is already satisfied:');
335
+ lines.push('');
336
+ if (guidedState) {
337
+ lines.push(`1. KNOWN PROJECT STATE — verified by the runner as this run started; TRUST it, do not re-check any of it with commands or file reads: git repository: ${guidedState.gitRepo ? 'yes' : 'NO'}; agent skills installed: ${guidedState.skillsInstalled ? 'yes' : 'no'}; environments configured: ${guidedState.environmentsConfigured ? 'yes' : 'no'}; infrastructure manifest (${manifestPath}): ${guidedState.infrastructurePresent ? 'present' : 'absent'}; operations: ${guidedState.opCount}${guidedState.onlyHello ? ' (only the default hello example)' : ''}. Skip every step this state already satisfies.${guidedState.gitRepo ? '' : ' There is NO git repository: STOP and tell the user to run `git init && git add -A && git commit -m "initial"` — nothing else can proceed without it.'}`);
338
+ }
339
+ else {
340
+ lines.push('1. READ THE GROUND TRUTH FIRST. Before acting, inspect what already exists so you can skip done work: whether this is a git repo, whether the agent skills are installed (.claude/skills/emberflow-basics or the codex/home equivalents), whether emberflow.environments.json exists, whether ' +
341
+ manifestPath +
342
+ ' exists, and whether the project has operations beyond the default hello example (list-workflows). If there is NO git repository, STOP and tell the user the exact command to run — `git init && git add -A && git commit -m "initial"` — because every later step depends on git snapshotting and nothing here can proceed without it.');
343
+ }
344
+ lines.push('');
345
+ lines.push(`2. GREENFIELD JUDGMENT + SCOUT. If ${manifestPath} is already present, SKIP this step (say so). Otherwise inspect the project the way the infrastructure scout does — dependencies (package.json + lockfiles, requirements.txt / pyproject.toml, go.mod, Gemfile, composer.json), config (docker-compose.yml, Dockerfile, .env.example NAMES only, prisma/schema.prisma, ORM/framework config), ORM schemas / migrations, env-var REFERENCES in source (process.env.X — NAMES only), HTTP clients / SDKs, and the project's own routes. If the project has real infrastructure (BROWNFIELD), run the full scout and WRITE ${manifestPath} describing it (version, scannedAt, greenfield:false, summary, items[] each with kind, name, evidence file+note, suggestedSecretRefs env-var NAMES only). If it is a bare scaffold (GREENFIELD — no databases, external APIs or providers), WRITE ${manifestPath} with "greenfield": true, empty items, and a one-line summary saying so — do NOT invent items to fill space. Manifest kinds: database, http-api, queue, cache, email, llm, auth, framework, storage, other.`);
346
+ lines.push('');
347
+ lines.push(`3. SKILLS. If the agent skills are already installed (from your ground-truth read), SKIP this step. Otherwise install them by running the init CLI — it is idempotent and, because this project already has a config, adds ONLY the skills (and any missing .gitignore lines), leaving your config and operations untouched:`);
348
+ lines.push(` node ${EMBERFLOW_BIN} init --local --no-launch --no-git ${projectLanguage === 'javascript' ? '--js' : '--ts'}`);
349
+ lines.push(` Report which skill files it created. The ${projectLanguage === 'javascript' ? '--js' : '--ts'} flag matches this project's language so init doesn't refuse to run against the existing config. If init CANNOT install them (the command errors), emit the exact command for the user to run themselves and move on — don't fail the whole run over skills.`);
350
+ lines.push('');
351
+ lines.push('4. CONNECTION PROOF. This run itself proves the agent CLI works end-to-end — you ARE that agent. No separate action, no model ids: one plain sentence at wrap-up like "Connection verified — I\'m running against your project."');
352
+ lines.push('');
353
+ lines.push(`5. ENVIRONMENTS INTERVIEW — ask FIRST (on the very first turn, before any other work; see TURN SHAPE), write after. If emberflow.environments.json already configures environments, SKIP (you may still refine on request). Otherwise end the FIRST turn with an emberflow-questions block whose FIRST question is whether to set environments up now at all: text like "Set up your environments now?", why like "Environments tell Emberflow where runs point — mock data now, your real dev or prod systems later. Takes a minute, and you can change it any time.", options ["Set up now",{"label":"Later — I'll do it from the setup checklist","action":"submit"}] (action "submit" sends the answer immediately, skipping the remaining questions). If they choose later: on the continuation turn do steps 2–4 only, then step 6, noting they can return via the Environments step in the setup checklist. If now: the SAME block also carries the detail questions (each with a one-line "why"): which environments (options like "dev + prod", "dev + staging + prod", custom), which is the default, which are production-like (protected), and their base URLs (custom text) — the form is a one-at-a-time wizard, so later answers simply arrive alongside; if they chose "Later" the other answers are ignored. A guided setup that decides everything itself and asks nothing has FAILED this step. CONTINUATION turn (answers arrive as your instruction): NOW do the working steps 2–4 (scout, skills, connection) and write emberflow.environments.json (STRUCTURE ONLY) from their answers — "defaultEnvironment" plus entries under "environments" (kebab/lower-case names); "secrets" is a LIST of key NAMES, never a value map; base URLs and non-secret config under "vars"; production-like environments get "protected": true (forces studio safe mode). Unknown values → clearly-named placeholders (e.g. "baseUrl": "https://CHANGE-ME.example.com"). Mention once that the file is gitignored. Then finish with step 6.`);
354
+ lines.push('');
355
+ lines.push('NEVER write secret or credential VALUES anywhere — not in emberflow.environments.json (which holds key NAMES only), not in ' +
356
+ manifestPath +
357
+ " (env-var NAMES only), and not in your output. Environment auth is set only via set-environment-auth (refs/names, never values); manifests carry names only. When auth or API keys are involved, scaffold at most the secret key NAMES and tell the user to enter the actual secret values in the studio's Manage Environment dialog — never in this chat.");
358
+ lines.push('');
359
+ lines.push('6. WRAP-UP + FIRST BUILD. One line per completed step (done/skipped) plus anything that genuinely needs the user (secret values to enter) — no prose report. Then END with a final emberflow-questions block: {"id":"first-build","text":"What do you want to build first?","options":[{"label":"Just look around","action":"finish"}],"custom":true}. If the user answers with a description, scaffold that operation in a continuation turn — create the operation file(s) following the emberflow-new-workflow skill (runnable in mock mode, scenarios included) and say which operation to open.');
360
+ break;
361
+ }
340
362
  case 'ask': {
341
363
  lines.push('Relevant skill: emberflow-basics (how the project is structured) — read it only if answering needs it.');
342
364
  lines.push('');
@@ -1,4 +1,4 @@
1
- import { type AgentIntent, type AvailableNode } from './prompt.js';
1
+ import { type AgentIntent, type AvailableNode, type GuidedSetupState } from './prompt.js';
2
2
  import type { InfrastructureManifest } from '../infrastructure.js';
3
3
  import type { AgentEvent, AgentKind } from './types.js';
4
4
  export interface StartAgentOptions {
@@ -41,6 +41,12 @@ export declare class AgentRunManager {
41
41
  * so agents REUSE known infrastructure instead of inventing parallel config.
42
42
  */
43
43
  private readonly infrastructure;
44
+ /**
45
+ * Getter for the runner-verified setup snapshot injected into guided-setup
46
+ * prompts as KNOWN state — so the agent doesn't burn its first turn
47
+ * re-deriving facts (git? skills? envs? ops?) the server already computed.
48
+ */
49
+ private readonly guidedState;
44
50
  private readonly runs;
45
51
  private activeRunId;
46
52
  constructor(projectDir: string, apisDir: string, pathOf: (id: string) => string | undefined,
@@ -62,7 +68,13 @@ export declare class AgentRunManager {
62
68
  * hasn't been scouted or the file is malformed. Injected as a prompt preamble
63
69
  * so agents REUSE known infrastructure instead of inventing parallel config.
64
70
  */
65
- infrastructure?: () => InfrastructureManifest | null);
71
+ infrastructure?: () => InfrastructureManifest | null,
72
+ /**
73
+ * Getter for the runner-verified setup snapshot injected into guided-setup
74
+ * prompts as KNOWN state — so the agent doesn't burn its first turn
75
+ * re-deriving facts (git? skills? envs? ops?) the server already computed.
76
+ */
77
+ guidedState?: () => GuidedSetupState | null);
66
78
  /** Validates the project + flow, snapshots git, spawns the adapter, and returns the new run's id. */
67
79
  start(intent: AgentIntent, opts?: StartAgentOptions): string;
68
80
  /** Drains the adapter's events into the run's buffer, fanning each out to live subscribers. */
@@ -79,6 +91,15 @@ export declare class AgentRunManager {
79
91
  reverted: string[];
80
92
  } | undefined;
81
93
  cancel(id: string): boolean;
94
+ /**
95
+ * Kills the active agent run's process tree, if any. Agent CLIs are spawned
96
+ * DETACHED (their own process group, so cancel can tree-kill their
97
+ * subprocesses) — which also means they survive the runner dying unless the
98
+ * shutdown path explicitly cancels them. An orphaned agent keeps editing the
99
+ * project long after the studio is gone; call this from the server's
100
+ * SIGINT/SIGTERM handlers.
101
+ */
102
+ shutdown(): void;
82
103
  /**
83
104
  * Evicts oldest terminal (done|error) runs beyond MAX_TERMINAL_RUNS so the
84
105
  * `runs` map — and each run's full event buffer — doesn't grow unbounded
@@ -3,6 +3,7 @@ import { changedFiles, diffSince, isGitRepo, revert as gitRevert, snapshot } fro
3
3
  import { buildPrompt } from './prompt.js';
4
4
  import { spawnCodex } from './codexAdapter.js';
5
5
  import { spawnClaude } from './claudeAdapter.js';
6
+ import { resolveAgentBin } from './detect.js';
6
7
  import { isPathWithin } from '../pathSafety.js';
7
8
  /** Thrown by `start()`; `status` maps directly onto the HTTP status the endpoint should return. */
8
9
  export class AgentStartError extends Error {
@@ -57,6 +58,7 @@ export class AgentRunManager {
57
58
  availableNodes;
58
59
  projectLanguage;
59
60
  infrastructure;
61
+ guidedState;
60
62
  runs = new Map();
61
63
  activeRunId = null;
62
64
  constructor(projectDir, apisDir, pathOf,
@@ -78,13 +80,20 @@ export class AgentRunManager {
78
80
  * hasn't been scouted or the file is malformed. Injected as a prompt preamble
79
81
  * so agents REUSE known infrastructure instead of inventing parallel config.
80
82
  */
81
- infrastructure = () => null) {
83
+ infrastructure = () => null,
84
+ /**
85
+ * Getter for the runner-verified setup snapshot injected into guided-setup
86
+ * prompts as KNOWN state — so the agent doesn't burn its first turn
87
+ * re-deriving facts (git? skills? envs? ops?) the server already computed.
88
+ */
89
+ guidedState = () => null) {
82
90
  this.projectDir = projectDir;
83
91
  this.apisDir = apisDir;
84
92
  this.pathOf = pathOf;
85
93
  this.availableNodes = availableNodes;
86
94
  this.projectLanguage = projectLanguage;
87
95
  this.infrastructure = infrastructure;
96
+ this.guidedState = guidedState;
88
97
  }
89
98
  /** Validates the project + flow, snapshots git, spawns the adapter, and returns the new run's id. */
90
99
  start(intent, opts = {}) {
@@ -92,7 +101,7 @@ export class AgentRunManager {
92
101
  throw new AgentStartError('An agent run is already active for this project', 409);
93
102
  }
94
103
  if (!isGitRepo(this.projectDir)) {
95
- throw new AgentStartError(`${this.projectDir} is not a git repository`, 400);
104
+ throw new AgentStartError(`${this.projectDir} is not a git repository — Emberflow snapshots agent changes with git so you can review and revert them. Run: git init && git add -A && git commit -m "initial" — then retry.`, 400);
96
105
  }
97
106
  // `new-operation` creates a brand-new flow, so there's no existing
98
107
  // flowId to validate against pathOf — it validates `location` instead.
@@ -109,10 +118,12 @@ export class AgentRunManager {
109
118
  }
110
119
  else if (intent.action === 'setup-auth' ||
111
120
  intent.action === 'setup-environments' ||
112
- intent.action === 'scout-infrastructure') {
121
+ intent.action === 'scout-infrastructure' ||
122
+ intent.action === 'guided-setup') {
113
123
  // No flow file: setup-auth targets an environment, setup-environments the
114
- // environments file, and scout-infrastructure reads the whole project and
115
- // writes emberflow/infrastructure.json none has a relPath to resolve.
124
+ // environments file, scout-infrastructure reads the whole project and
125
+ // writes emberflow/infrastructure.json, and guided-setup orchestrates all
126
+ // of setup across project-root files — none has a relPath to resolve.
116
127
  relPath = '';
117
128
  }
118
129
  else if (intent.action === 'ask') {
@@ -145,16 +156,19 @@ export class AgentRunManager {
145
156
  }
146
157
  this.pruneTerminalRuns();
147
158
  const snap = snapshot(this.projectDir);
148
- const prompt = buildPrompt(intent, this.apisDir, relPath, this.availableNodes(), this.projectLanguage, this.infrastructure());
159
+ const prompt = buildPrompt(intent, this.apisDir, relPath, this.availableNodes(), this.projectLanguage, this.infrastructure(), intent.action === 'guided-setup' ? this.guidedState() : null);
160
+ // Env override wins (tests stub agents this way); otherwise use the NEWEST
161
+ // binary detection found across PATH + known install locations — a PATH
162
+ // shim pinned to a stale release must not shadow a newer install elsewhere.
149
163
  const adapter = opts.agent === 'claude'
150
164
  ? spawnClaude(prompt, this.projectDir, {
151
165
  model: opts.model,
152
- bin: process.env.EMBERFLOW_CLAUDE_BIN,
166
+ bin: process.env.EMBERFLOW_CLAUDE_BIN ?? resolveAgentBin('claude'),
153
167
  })
154
168
  : spawnCodex(prompt, this.projectDir, {
155
169
  model: opts.model,
156
170
  reasoning: opts.reasoning ?? 'medium',
157
- bin: process.env.EMBERFLOW_CODEX_BIN,
171
+ bin: process.env.EMBERFLOW_CODEX_BIN ?? resolveAgentBin('codex'),
158
172
  });
159
173
  const id = randomUUID();
160
174
  const run = { id, snapshot: snap, adapter, buffer: [], listeners: new Set(), status: 'running' };
@@ -237,6 +251,20 @@ export class AgentRunManager {
237
251
  run.adapter.cancel();
238
252
  return true;
239
253
  }
254
+ /**
255
+ * Kills the active agent run's process tree, if any. Agent CLIs are spawned
256
+ * DETACHED (their own process group, so cancel can tree-kill their
257
+ * subprocesses) — which also means they survive the runner dying unless the
258
+ * shutdown path explicitly cancels them. An orphaned agent keeps editing the
259
+ * project long after the studio is gone; call this from the server's
260
+ * SIGINT/SIGTERM handlers.
261
+ */
262
+ shutdown() {
263
+ if (!this.activeRunId)
264
+ return;
265
+ const run = this.runs.get(this.activeRunId);
266
+ run?.adapter.cancel();
267
+ }
240
268
  /**
241
269
  * Evicts oldest terminal (done|error) runs beyond MAX_TERMINAL_RUNS so the
242
270
  * `runs` map — and each run's full event buffer — doesn't grow unbounded
@@ -4,6 +4,7 @@ export type { ApiTree, FolderNode, ApiNode, OpSummary } from './apiStore.js';
4
4
  /** SSE-shaped events emitted on GET /runs/:id/events — mirrors server/runRegistry.ts RunEvent. */
5
5
  export type RunEvent = {
6
6
  type: 'nodeState';
7
+ workflowId: string;
7
8
  nodeId: string;
8
9
  state: NodeRunState;
9
10
  } | {
@@ -1,6 +1,11 @@
1
- const DEFAULT_BASE_URL = 'http://127.0.0.1:8092';
2
1
  function baseUrl() {
3
- return process.env.EMBERFLOW_RUNNER_URL ?? DEFAULT_BASE_URL;
2
+ // EMBERFLOW_RUNNER_PORT is how the runner itself is launched on a custom
3
+ // port (bin/commands.ts sets it for `emberflow dev --port`), and child
4
+ // processes — including agent runs invoking this CLI — inherit it. Without
5
+ // honoring it here, an agent spawned by a runner on :8095 would call back
6
+ // to the default :8092 and report the runner down.
7
+ return (process.env.EMBERFLOW_RUNNER_URL ??
8
+ `http://127.0.0.1:${process.env.EMBERFLOW_RUNNER_PORT ?? 8092}`);
4
9
  }
5
10
  /** Thrown when the runner cannot be reached at all (connection refused, DNS, etc). */
6
11
  export class RunnerUnreachableError extends Error {