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/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 }}
@@ -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
+ });