symphifo 0.1.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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +394 -0
- package/SYMPHIFO.md +171 -0
- package/WORKFLOW.md +39 -0
- package/bin/symphifo.js +37 -0
- package/package.json +46 -0
- package/src/cli.ts +213 -0
- package/src/dashboard/app.js +1390 -0
- package/src/dashboard/index.html +139 -0
- package/src/dashboard/styles.css +1528 -0
- package/src/fixtures/local-issues.json +13 -0
- package/src/integrations/catalog.ts +151 -0
- package/src/mcp/server.ts +1237 -0
- package/src/routing/capability-resolver.ts +390 -0
- package/src/runtime/agent.ts +1050 -0
- package/src/runtime/api-server.ts +306 -0
- package/src/runtime/constants.ts +102 -0
- package/src/runtime/helpers.ts +134 -0
- package/src/runtime/issues.ts +456 -0
- package/src/runtime/logger.ts +59 -0
- package/src/runtime/providers.ts +310 -0
- package/src/runtime/run-local.ts +146 -0
- package/src/runtime/scheduler.ts +214 -0
- package/src/runtime/skills.ts +55 -0
- package/src/runtime/store.ts +313 -0
- package/src/runtime/types.ts +274 -0
- package/src/runtime/workflow.ts +185 -0
package/SYMPHIFO.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Symphifo local runtime reference
|
|
2
|
+
|
|
3
|
+
This repository runs Symphifo as a pure TypeScript local orchestrator with no external tracker dependency.
|
|
4
|
+
|
|
5
|
+
## What this package provides
|
|
6
|
+
|
|
7
|
+
- Filesystem-backed orchestration through the local persistence runtime.
|
|
8
|
+
- Optional seed issues from `src/fixtures/local-issues.json`.
|
|
9
|
+
- Durable tracker state that can also start empty and accept work over HTTP.
|
|
10
|
+
- Local workspace snapshots for reproducible execution.
|
|
11
|
+
- Queue runner with concurrency, retries, retry backoff, and stale-run recovery.
|
|
12
|
+
- Local event log, API, and dashboard through the `s3db.js` `ApiPlugin`.
|
|
13
|
+
- Multi-agent pipelines with `codex` and `claude`.
|
|
14
|
+
|
|
15
|
+
## Relevant files
|
|
16
|
+
|
|
17
|
+
- Workflow template: [WORKFLOW.md](./WORKFLOW.md)
|
|
18
|
+
- Published entrypoint: [bin/symphifo.js](./bin/symphifo.js)
|
|
19
|
+
- CLI router: [src/cli.ts](./src/cli.ts)
|
|
20
|
+
- Runtime engine: [src/runtime/run-local.ts](./src/runtime/run-local.ts)
|
|
21
|
+
- Dashboard: [src/dashboard/index.html](./src/dashboard/index.html)
|
|
22
|
+
|
|
23
|
+
## Environment variables
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export SYMPHIFO_TRACKER_KIND=filesystem
|
|
27
|
+
export SYMPHIFO_WORKSPACE_ROOT=$PWD
|
|
28
|
+
export SYMPHIFO_PERSISTENCE=$PWD
|
|
29
|
+
export SYMPHIFO_ISSUES_FILE=/path/to/issues.json
|
|
30
|
+
export SYMPHIFO_ISSUES_JSON='[{"id":"LOCAL-1","title":"...","description":"...","state":"Todo"}]'
|
|
31
|
+
export SYMPHIFO_AGENT_COMMAND='codex run --json "$SYMPHIFO_ISSUE_JSON"'
|
|
32
|
+
export SYMPHIFO_AGENT_PROVIDER=codex
|
|
33
|
+
export SYMPHIFO_WORKER_CONCURRENCY=2
|
|
34
|
+
export SYMPHIFO_MAX_ATTEMPTS=3
|
|
35
|
+
export SYMPHIFO_AGENT_MAX_TURNS=4
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`SYMPHIFO_AGENT_COMMAND` is required unless `WORKFLOW.md` provides `codex.command` or `claude.command`.
|
|
39
|
+
|
|
40
|
+
Node requirement:
|
|
41
|
+
|
|
42
|
+
- Node.js 23 or newer
|
|
43
|
+
|
|
44
|
+
## Start examples
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx symphifo
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Default state location:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
./.symphifo/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Override the persistence root:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx symphifo --persistence /path/to/root
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run the MCP server:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx symphifo mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run a single cycle:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx symphifo --once
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run with the API and dashboard:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx symphifo --port 4040 --concurrency 2 --attempts 3
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Runtime behavior
|
|
81
|
+
|
|
82
|
+
- Local bootstrap creates a source snapshot under `./.symphifo/source`.
|
|
83
|
+
- Issues are loaded from the configured JSON source when available.
|
|
84
|
+
- Workflow is rendered to `./.symphifo/WORKFLOW.local.md`.
|
|
85
|
+
- Runtime state is stored under `./.symphifo/s3db/` by the `s3db.js` `FileSystemClient`.
|
|
86
|
+
- Event log is stored in `./.symphifo/symphifo-local.log`.
|
|
87
|
+
- `WORKFLOW.md` front matter and Markdown body define the execution contract when present.
|
|
88
|
+
- `hooks.after_create` runs once for a new issue workspace; otherwise the runtime copies the local source snapshot.
|
|
89
|
+
- `hooks.before_run` and `hooks.after_run` can wrap each agent turn.
|
|
90
|
+
- `agent.provider` can be `codex` or `claude`.
|
|
91
|
+
- `agent.providers[]` can mix both in one pipeline.
|
|
92
|
+
- `agent.profile` resolves to local profile files from workspace or home directories.
|
|
93
|
+
- `routing.enabled` can disable automatic task routing.
|
|
94
|
+
- `routing.priorities` can override the default scheduler order by capability category.
|
|
95
|
+
- `routing.overrides[]` can override the automatic provider/profile selection for matching tasks.
|
|
96
|
+
- `routing.overrides[].match.paths` can force routing based on target directories or files.
|
|
97
|
+
- Issue payloads can carry `paths[]` so routing can use the real change surface, not only text and labels.
|
|
98
|
+
- When `paths[]` is omitted, Symphifo infers routing hints from path-like text mentions and from files changed inside an existing persisted workspace.
|
|
99
|
+
- Symphifo derives labels like `capability:<category>` and `overlay:<name>` from the routing result for queue triage and visibility.
|
|
100
|
+
- The rendered prompt is written to `symphifo-prompt.md` and exported through `SYMPHIFO_PROMPT` and `SYMPHIFO_PROMPT_FILE`.
|
|
101
|
+
- Each issue runs as a multi-turn session controlled by `agent.max_turns`.
|
|
102
|
+
- Each turn exports `SYMPHIFO_AGENT_PROVIDER`, `SYMPHIFO_AGENT_ROLE`, `SYMPHIFO_AGENT_PROFILE`, `SYMPHIFO_AGENT_PROFILE_FILE`, `SYMPHIFO_AGENT_PROFILE_INSTRUCTIONS`, `SYMPHIFO_SESSION_ID`, `SYMPHIFO_SESSION_KEY`, `SYMPHIFO_TURN_INDEX`, `SYMPHIFO_MAX_TURNS`, `SYMPHIFO_TURN_PROMPT`, `SYMPHIFO_TURN_PROMPT_FILE`, `SYMPHIFO_PREVIOUS_OUTPUT`, and `SYMPHIFO_RESULT_FILE`.
|
|
103
|
+
- The agent can continue, finish, block, or fail by printing `SYMPHIFO_STATUS=...` or by writing `symphifo-result.json`.
|
|
104
|
+
- Session and pipeline state are persisted in `s3db`.
|
|
105
|
+
- Workspace JSON artifacts are temporary CLI handoff files, not the source of truth.
|
|
106
|
+
- The `s3db` resources are partitioned for the main operational lookups (`state`, `capabilityCategory`, `issueId`, `kind`, `attempt`, `provider/role`).
|
|
107
|
+
- The scheduler advances one turn per execution slot and resumes persisted `In Progress` work.
|
|
108
|
+
- When issue priority ties, the scheduler prefers more critical capability categories first (`security`, `bugfix`, `backend`, `devops`, `frontend-ui`, `architecture`, `documentation`, `default`) unless `routing.priorities` overrides that order.
|
|
109
|
+
- `npx symphifo mcp` keeps the scheduler alive even without the dashboard port.
|
|
110
|
+
- `npx symphifo mcp` starts a stdio MCP server backed by the same durable `s3db` state as the runtime.
|
|
111
|
+
- frontend-heavy tasks automatically carry stricter review overlays such as `impeccable` when matched by the capability resolver.
|
|
112
|
+
|
|
113
|
+
## MCP capabilities
|
|
114
|
+
|
|
115
|
+
Resources:
|
|
116
|
+
|
|
117
|
+
- `symphifo://guide/overview`
|
|
118
|
+
- `symphifo://guide/runtime`
|
|
119
|
+
- `symphifo://guide/integration`
|
|
120
|
+
- `symphifo://state/summary`
|
|
121
|
+
- `symphifo://issues`
|
|
122
|
+
- `symphifo://workspace/workflow`
|
|
123
|
+
- `symphifo://issue/<id>`
|
|
124
|
+
|
|
125
|
+
Tools:
|
|
126
|
+
|
|
127
|
+
- `symphifo.status`
|
|
128
|
+
- `symphifo.list_issues`
|
|
129
|
+
- `symphifo.create_issue`
|
|
130
|
+
- `symphifo.update_issue_state`
|
|
131
|
+
- `symphifo.integration_config`
|
|
132
|
+
|
|
133
|
+
Prompts:
|
|
134
|
+
|
|
135
|
+
- `symphifo-integrate-client`
|
|
136
|
+
- `symphifo-plan-issue`
|
|
137
|
+
- `symphifo-review-workflow`
|
|
138
|
+
|
|
139
|
+
Recommended MCP client config:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"mcpServers": {
|
|
144
|
+
"symphifo": {
|
|
145
|
+
"command": "npx",
|
|
146
|
+
"args": ["symphifo", "mcp", "--workspace", "/path/to/workspace", "--persistence", "/path/to/workspace"]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## HTTP surface
|
|
153
|
+
|
|
154
|
+
Compatibility routes:
|
|
155
|
+
|
|
156
|
+
- `/api/state`
|
|
157
|
+
- `/api/issues` with optional `state` and `capabilityCategory` query filters
|
|
158
|
+
- `POST /api/issues`
|
|
159
|
+
- `/api/issue/:id/pipeline`
|
|
160
|
+
- `/api/issue/:id/sessions`
|
|
161
|
+
- `/api/events` with optional `issueId`, `kind`, and `since` query filters
|
|
162
|
+
- `/api/health`
|
|
163
|
+
|
|
164
|
+
Generated documentation and native resources:
|
|
165
|
+
|
|
166
|
+
- `/docs`
|
|
167
|
+
- `/symphifo_runtime_state`
|
|
168
|
+
- `/symphifo_issues`
|
|
169
|
+
- `/symphifo_events`
|
|
170
|
+
- `/symphifo_agent_sessions`
|
|
171
|
+
- `/symphifo_agent_pipelines`
|
package/WORKFLOW.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
tracker:
|
|
3
|
+
kind: filesystem
|
|
4
|
+
workspace:
|
|
5
|
+
root: ./.symphifo/workspaces
|
|
6
|
+
agent:
|
|
7
|
+
provider: codex
|
|
8
|
+
profile: ""
|
|
9
|
+
max_concurrent_agents: 2
|
|
10
|
+
max_attempts: 3
|
|
11
|
+
max_turns: 4
|
|
12
|
+
providers:
|
|
13
|
+
- provider: claude
|
|
14
|
+
role: planner
|
|
15
|
+
profile: ""
|
|
16
|
+
- provider: codex
|
|
17
|
+
role: executor
|
|
18
|
+
profile: ""
|
|
19
|
+
- provider: claude
|
|
20
|
+
role: reviewer
|
|
21
|
+
profile: ""
|
|
22
|
+
codex:
|
|
23
|
+
command: ""
|
|
24
|
+
claude:
|
|
25
|
+
command: ""
|
|
26
|
+
routing:
|
|
27
|
+
enabled: true
|
|
28
|
+
priorities: {}
|
|
29
|
+
overrides: []
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
You are working on {{ issue.identifier }}.
|
|
33
|
+
|
|
34
|
+
Title: {{ issue.title }}
|
|
35
|
+
Description:
|
|
36
|
+
{{ issue.description }}
|
|
37
|
+
|
|
38
|
+
Target paths:
|
|
39
|
+
{{ issue.paths }}
|
package/bin/symphifo.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { cwd, env, exit, argv, execPath } from "node:process";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const packageRoot = resolve(__dirname, "..");
|
|
11
|
+
const workspaceRoot = env.SYMPHIFO_WORKSPACE_ROOT ?? cwd();
|
|
12
|
+
const cliScript = resolve(packageRoot, "src", "cli.ts");
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const tsxCli = require.resolve("tsx/cli");
|
|
15
|
+
|
|
16
|
+
const child = spawn(execPath, [tsxCli, cliScript, ...argv.slice(2)], {
|
|
17
|
+
cwd: workspaceRoot,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
env: {
|
|
20
|
+
...env,
|
|
21
|
+
SYMPHIFO_WORKSPACE_ROOT: workspaceRoot,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on("exit", (code, signal) => {
|
|
26
|
+
if (signal) {
|
|
27
|
+
process.kill(process.pid, signal);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exit(code ?? 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on("error", (error) => {
|
|
35
|
+
console.error(`Failed to start symphifo CLI: ${String(error)}`);
|
|
36
|
+
exit(1);
|
|
37
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "symphifo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Filesystem-backed local Symphifo orchestrator with a TypeScript CLI, MCP mode, and multi-agent Codex or Claude workflows.",
|
|
7
|
+
"bin": {
|
|
8
|
+
"symphifo": "./bin/symphifo.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"WORKFLOW.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"SYMPHIFO.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/forattini-dev/symphifo.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/forattini-dev/symphifo/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/forattini-dev/symphifo#readme",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=23"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"cli-args-parser": "^1.0.6",
|
|
35
|
+
"pino": "^10.3.1",
|
|
36
|
+
"pino-pretty": "^13.1.3",
|
|
37
|
+
"raffel": "^1.0.7",
|
|
38
|
+
"s3db.js": "^21.2.2",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"yaml": "^2.8.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"start": "node ./bin/symphifo.js",
|
|
44
|
+
"mcp": "node ./bin/symphifo.js mcp"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { cwd, env, execPath, exit, kill, pid } from "node:process";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { createCLI, type CommandParseResult } from "cli-args-parser";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const packageRoot = resolve(__dirname, "..");
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")) as {
|
|
14
|
+
name?: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
};
|
|
18
|
+
const runtimeScript = resolve(packageRoot, "src", "runtime", "run-local.ts");
|
|
19
|
+
const mcpScript = resolve(packageRoot, "src", "mcp", "server.ts");
|
|
20
|
+
const tsxCli = require.resolve("tsx/cli");
|
|
21
|
+
|
|
22
|
+
const commonOptions = {
|
|
23
|
+
workspace: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Target workspace root. Defaults to the current directory.",
|
|
26
|
+
},
|
|
27
|
+
persistence: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Persistence root. Defaults to the current directory.",
|
|
30
|
+
},
|
|
31
|
+
port: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Start the local API/dashboard on the provided port.",
|
|
34
|
+
},
|
|
35
|
+
concurrency: {
|
|
36
|
+
type: "number",
|
|
37
|
+
description: "Maximum number of concurrent workers.",
|
|
38
|
+
},
|
|
39
|
+
attempts: {
|
|
40
|
+
type: "number",
|
|
41
|
+
description: "Maximum attempts per issue.",
|
|
42
|
+
},
|
|
43
|
+
poll: {
|
|
44
|
+
type: "number",
|
|
45
|
+
description: "Scheduler interval in milliseconds.",
|
|
46
|
+
},
|
|
47
|
+
once: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
description: "Process one scheduler cycle and exit.",
|
|
50
|
+
default: false,
|
|
51
|
+
},
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
function getStringOption(result: CommandParseResult, key: keyof typeof commonOptions): string | undefined {
|
|
55
|
+
const value = result.options[key];
|
|
56
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getNumberOption(result: CommandParseResult, key: keyof typeof commonOptions): number | undefined {
|
|
60
|
+
const value = result.options[key];
|
|
61
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getBooleanOption(result: CommandParseResult, key: keyof typeof commonOptions): boolean {
|
|
65
|
+
return result.options[key] === true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildRuntimeArgs(result: CommandParseResult): string[] {
|
|
69
|
+
const runtimeArgs: string[] = [];
|
|
70
|
+
const workspace = getStringOption(result, "workspace");
|
|
71
|
+
const persistence = getStringOption(result, "persistence");
|
|
72
|
+
const port = getNumberOption(result, "port");
|
|
73
|
+
const concurrency = getNumberOption(result, "concurrency");
|
|
74
|
+
const attempts = getNumberOption(result, "attempts");
|
|
75
|
+
const poll = getNumberOption(result, "poll");
|
|
76
|
+
|
|
77
|
+
if (workspace) {
|
|
78
|
+
runtimeArgs.push("--workspace", workspace);
|
|
79
|
+
}
|
|
80
|
+
if (persistence) {
|
|
81
|
+
runtimeArgs.push("--persistence", persistence);
|
|
82
|
+
}
|
|
83
|
+
if (typeof port === "number") {
|
|
84
|
+
runtimeArgs.push("--port", String(port));
|
|
85
|
+
}
|
|
86
|
+
if (typeof concurrency === "number") {
|
|
87
|
+
runtimeArgs.push("--concurrency", String(concurrency));
|
|
88
|
+
}
|
|
89
|
+
if (typeof attempts === "number") {
|
|
90
|
+
runtimeArgs.push("--attempts", String(attempts));
|
|
91
|
+
}
|
|
92
|
+
if (typeof poll === "number") {
|
|
93
|
+
runtimeArgs.push("--poll", String(poll));
|
|
94
|
+
}
|
|
95
|
+
if (getBooleanOption(result, "once")) {
|
|
96
|
+
runtimeArgs.push("--once");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return runtimeArgs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runRuntime(mode: "cli" | "mcp", result: CommandParseResult): Promise<void> {
|
|
103
|
+
const workspace = getStringOption(result, "workspace");
|
|
104
|
+
const workspaceRoot = resolve(workspace ?? env.SYMPHIFO_WORKSPACE_ROOT ?? cwd());
|
|
105
|
+
const runtimeArgs = buildRuntimeArgs(result);
|
|
106
|
+
|
|
107
|
+
const outcome = await new Promise<{ code?: number | null; signal?: NodeJS.Signals | null }>((resolvePromise, rejectPromise) => {
|
|
108
|
+
const child = spawn(execPath, [tsxCli, runtimeScript, ...runtimeArgs], {
|
|
109
|
+
cwd: workspaceRoot,
|
|
110
|
+
stdio: "inherit",
|
|
111
|
+
env: {
|
|
112
|
+
...env,
|
|
113
|
+
SYMPHIFO_INTERFACE: mode,
|
|
114
|
+
SYMPHIFO_WORKSPACE_ROOT: workspaceRoot,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.on("exit", (code, signal) => {
|
|
119
|
+
resolvePromise({ code, signal });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
child.on("error", (error) => {
|
|
123
|
+
rejectPromise(error);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (outcome.signal) {
|
|
128
|
+
kill(pid, outcome.signal);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof outcome.code === "number" && outcome.code !== 0) {
|
|
133
|
+
exit(outcome.code);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runMcpServer(result: CommandParseResult): Promise<void> {
|
|
138
|
+
const workspace = getStringOption(result, "workspace");
|
|
139
|
+
const persistence = getStringOption(result, "persistence");
|
|
140
|
+
const workspaceRoot = resolve(workspace ?? env.SYMPHIFO_WORKSPACE_ROOT ?? cwd());
|
|
141
|
+
const persistenceRoot = resolve(persistence ?? env.SYMPHIFO_PERSISTENCE ?? workspaceRoot);
|
|
142
|
+
|
|
143
|
+
const outcome = await new Promise<{ code?: number | null; signal?: NodeJS.Signals | null }>((resolvePromise, rejectPromise) => {
|
|
144
|
+
const child = spawn(execPath, [tsxCli, mcpScript], {
|
|
145
|
+
cwd: workspaceRoot,
|
|
146
|
+
stdio: "inherit",
|
|
147
|
+
env: {
|
|
148
|
+
...env,
|
|
149
|
+
SYMPHIFO_WORKSPACE_ROOT: workspaceRoot,
|
|
150
|
+
SYMPHIFO_PERSISTENCE: persistenceRoot,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
child.on("exit", (code, signal) => {
|
|
155
|
+
resolvePromise({ code, signal });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.on("error", (error) => {
|
|
159
|
+
rejectPromise(error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (outcome.signal) {
|
|
164
|
+
kill(pid, outcome.signal);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof outcome.code === "number" && outcome.code !== 0) {
|
|
169
|
+
exit(outcome.code);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cli = createCLI({
|
|
174
|
+
name: packageJson.name ?? "symphifo",
|
|
175
|
+
version: packageJson.version ?? "0.0.0",
|
|
176
|
+
description: packageJson.description ?? "Filesystem-backed local multi-agent orchestrator.",
|
|
177
|
+
commands: {
|
|
178
|
+
run: {
|
|
179
|
+
description: "Run the local Symphifo runtime with the dashboard/API enabled when --port is provided.",
|
|
180
|
+
options: commonOptions,
|
|
181
|
+
handler: (result) => runRuntime("cli", result),
|
|
182
|
+
},
|
|
183
|
+
mcp: {
|
|
184
|
+
description: "Run a Symphifo MCP server over stdio with resources, tools, and prompts backed by the local durable store.",
|
|
185
|
+
options: commonOptions,
|
|
186
|
+
handler: (result) => runMcpServer(result),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
function normalizeArgs(rawArgs: string[]): string[] {
|
|
192
|
+
if (rawArgs.length === 0) {
|
|
193
|
+
return ["run"];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const first = rawArgs[0];
|
|
197
|
+
if (["--help", "-h", "help", "--version", "-v", "version"].includes(first)) {
|
|
198
|
+
return rawArgs;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (first.startsWith("-")) {
|
|
202
|
+
return ["run", ...rawArgs];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return rawArgs;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const args = normalizeArgs(process.argv.slice(2));
|
|
209
|
+
|
|
210
|
+
cli.run(args).catch((error) => {
|
|
211
|
+
console.error(`Failed to start symphifo CLI: ${String(error)}`);
|
|
212
|
+
exit(1);
|
|
213
|
+
});
|