@xdelivered/emberflow 0.2.0 → 0.4.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.
- package/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +185 -14
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/package.json +27 -6
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +189 -13
- package/server/nodeRun.test.ts +189 -0
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +383 -312
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-CAIDjNhv.css +1 -0
- package/studio-dist/assets/index-CGwEx82J.js +65 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-model-process/SKILL.md +82 -9
- package/templates/skills/emberflow-review-workflow/SKILL.md +37 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
const VERSION_RE = /\d+\.\d+\.\d+/;
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
23
|
-
* each
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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,
|
|
115
|
-
// writes emberflow/infrastructure.json
|
|
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
|
package/dist/server/client.d.ts
CHANGED
|
@@ -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
|
} | {
|
package/dist/server/client.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
const DEFAULT_BASE_URL = 'http://127.0.0.1:8092';
|
|
2
1
|
function baseUrl() {
|
|
3
|
-
|
|
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 {
|