Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ On Linux x86_64, install the latest packaged Symphony release into your user pro
curl -fsSL https://raw.githubusercontent.com/BetterAndBetterII/symphony/main/scripts/install.sh | sh
```

After installation, run `symphony` from any project directory. It will create `WORKFLOW.md` when
missing and then boot the service from that directory.
After installation, run `symphony` from any project directory. If `WORKFLOW.md` is missing,
interactive terminals offer a guided GitHub Project bootstrap (with a baseline-template fallback),
while non-interactive runs still write the baseline template before booting the service.

To pin a specific release, export `SYMPHONY_VERSION` before invoking the installer.

Expand Down
135 changes: 132 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,11 @@ Unless otherwise noted, Sections 17.1 through 17.7 are `Core Conformance`. Bulle

- CLI accepts an optional positional workflow path argument (`path-to-WORKFLOW.md`)
- CLI uses `./WORKFLOW.md` when no workflow path argument is provided
- CLI errors on nonexistent explicit workflow path or missing default `./WORKFLOW.md`
- CLI errors on nonexistent explicit workflow path
- When the default `./WORKFLOW.md` is missing, CLI bootstraps it before startup per Section 19.3
- If guided bootstrap is implemented and stdin/stdout are interactive TTYs, the default-path
bootstrap may present a terminal wizard before writing `./WORKFLOW.md`
- Bootstrap failures do not leave a partially written `WORKFLOW.md`
- CLI surfaces startup failure cleanly
- GitHub startup failures surface actionable `gh` remediation when implicit `gh` auth is selected
- CLI exits with success when application starts and shuts down normally
Expand Down Expand Up @@ -2262,8 +2266,13 @@ To support quick starts, an implementation that ships this profile SHOULD make `
from any directory:

- If `./WORKFLOW.md` exists: start Symphony using that workflow.
- If `./WORKFLOW.md` is missing: create a default `WORKFLOW.md` in the current directory, then
start Symphony using that newly created file.
- If an explicit workflow path is passed and that file is missing: fail without creating any new
file.
- If `./WORKFLOW.md` is missing and stdin/stdout are not interactive TTYs: create the baseline
`WORKFLOW.md` in the current directory, then start Symphony using that newly created file.
- If `./WORKFLOW.md` is missing and stdin/stdout are interactive TTYs: offer a guided bootstrap
flow before startup. That flow must still expose the baseline template path as an operator
choice.

The default `WORKFLOW.md` SHOULD:

Expand All @@ -2273,6 +2282,117 @@ The default `WORKFLOW.md` SHOULD:
- Include clear inline comments or prompt text that indicates where to customize project-specific
settings (project owner/number, clone URL, etc).

#### 19.3.1 Guided GitHub Project Bootstrap

If the interactive bootstrap path is implemented for GitHub-backed setups, the recommended terminal
wizard contract is:

1. Present an initial mode picker with these semantic choices:
- guided GitHub Project setup
- write the baseline non-interactive template
- cancel startup without writing a file
2. Treat all wizard answers as parsed domain values rather than free-form config fragments. A
conforming implementation should normalize selection answers into typed values before rendering
`WORKFLOW.md` (for example a selected project ref, a chosen approval policy, and a chosen
sandbox mode).
3. Only write `./WORKFLOW.md` after the guided flow reaches a complete, internally consistent
result. Failed GitHub reads/writes or abandoned prompts must not leave a partial file behind.

The guided GitHub path SHOULD resolve auth for bootstrap API calls in this order:

1. `GITHUB_TOKEN` when present.
2. `GH_TOKEN` when present.
3. `gh auth token --hostname <tracker-host>` after verifying the active account/scopes.

The generated GitHub-backed `WORKFLOW.md` SHOULD include concrete values for:

- `tracker.kind: github_project`
- `tracker.endpoint: https://api.github.com/graphql` unless a different host was explicitly chosen
- `tracker.api_key: $GITHUB_TOKEN`
- `tracker.project_owner`: the selected or newly created project owner login
- `tracker.project_number`: the selected or newly created ProjectV2 number
- `tracker.project_field_status`: the status field name reconciled by bootstrap (default `Status`)
- `tracker.active_states`: `Todo`, `Spec`, `In Progress`, `Rework`, `In Review`, `Merging`
- `tracker.terminal_states`: `Done`, `Canceled`, `Duplicated`
- `codex.approval_policy`: the operator-selected approval policy
- `codex.thread_sandbox`: the operator-selected thread sandbox mode

`Backlog` should still be provisioned in the ProjectV2 status field for manual triage, but it is
intentionally excluded from `tracker.active_states` so Symphony does not auto-dispatch backlog work
until a human moves it into an active state.

The remaining baseline fields (workspace root, hooks, prompt body, polling, and server defaults)
should stay aligned with the non-interactive template so packaged releases keep one shared runtime
contract.

#### 19.3.2 GitHub Project Discovery and Creation

The guided GitHub bootstrap SHOULD list accessible ProjectV2 boards visible to the authenticated
viewer across:

- the viewer-owned projects, and
- organization-owned projects for organizations returned by the viewer membership query.

The selection UI should present enough metadata to distinguish boards at a glance (owner login,
project title, project number, and URL or equivalent identifier).

If the operator chooses to create a new project, the guided flow SHOULD:

1. Let the operator choose an owner from the owners they can create projects under.
2. Accept a non-empty project title.
3. Create a ProjectV2 via GitHub GraphQL.
4. Ensure the workflow status field contains at least these canonical Symphony options, in this
semantic order:
- `Backlog`
Comment thread
BetterAndBetterII marked this conversation as resolved.
- `Todo`
- `Spec`
- `In Progress`
- `Rework`
- `In Review`
- `Merging`
- `Done`
- `Canceled`
- `Duplicated`

For an existing selected project, the bootstrap SHOULD reconcile the configured single-select
status field so the required Symphony options are present without discarding unrelated existing
options. If no compatible single-select field exists, bootstrap may create one and must write the
chosen field name back into `tracker.project_field_status`.

Status option colors/descriptions are implementation-defined; correctness is defined by the option
names and by writing the matching field name into `WORKFLOW.md`.

#### 19.3.3 Codex Defaults Selection

The guided bootstrap SHOULD let the operator choose the Codex defaults written into the generated
workflow.

Recommended minimum choices:

- `codex.approval_policy`: `untrusted`, `on-failure`, `on-request`, `never`
- `codex.thread_sandbox`: `read-only`, `workspace-write`, `danger-full-access`

The UI may describe one option in each group as the recommended default, but it should only offer
values the runtime already understands so the emitted workflow is valid without follow-up edits.

#### 19.3.4 Failure Modes and Guidance

Guided bootstrap failures for GitHub-backed setup SHOULD be typed and operator-actionable:

- Missing auth / missing `gh` / missing scopes: surface one concrete remediation command (for
example `gh auth login --hostname github.com --scopes repo,project,read:org` or
`gh auth refresh --hostname github.com --scopes repo,project,read:org`).
- Permission failures (for example project listing or creation forbidden): identify the GitHub
operation that failed and explain that the authenticated principal needs ProjectV2 access for the
selected owner.
- GraphQL or transport failures: include the failed operation context and concise API detail.
- If project creation succeeds but later field reconciliation fails, report the created project
identifier so the operator can clean it up manually before retrying.

These failures should stop the guided path without writing `WORKFLOW.md`. They may return the
operator to the initial mode picker, but they should never silently fall back to a guessed project
configuration.

### 19.4 GitHub Release Automation

For GitHub-hosted repos, the recommended automation is:
Expand Down Expand Up @@ -2301,6 +2421,8 @@ Suggested implementation milestones for this profile:
3. Provide a first-run bootstrap for `WORKFLOW.md`
- Ensure `symphony` creates `./WORKFLOW.md` if missing, using a default template that is valid
and runnable once required credentials are present.
- For interactive terminals, offer the guided GitHub Project bootstrap flow defined in Section
19.3 while keeping the baseline template path available.
4. Automate releases on GitHub Actions
- Add workflows that create `v<version>` tags and publish GitHub Releases with the built assets
attached.
Expand All @@ -2313,10 +2435,17 @@ Suggested validation for this profile:
- Local validation:
- Build the release artifact and start `symphony` in a clean directory that does not contain a
`WORKFLOW.md`; verify that it creates the file and starts the service.
- In an interactive terminal with GitHub auth configured, verify the guided bootstrap can both
select an existing ProjectV2 and create a new sandbox ProjectV2, then inspect the generated
`WORKFLOW.md` for the selected project metadata and Codex defaults.
- Verify that the installed `symphony` works when Elixir/Mix are not present on `$PATH` (for
example by running in a minimal container/VM).
- CI validation:
- Ensure the release workflow builds at least one target and uploads an asset to a GitHub Release.
- Failure-mode validation:
- Run the guided bootstrap without GitHub auth (and with insufficient scopes, if feasible) and
confirm it fails with explicit remediation guidance and does not write `WORKFLOW.md`.
- Force a GitHub Project permission or API failure during guided bootstrap and confirm the error
identifies the failed operation with actionable context.
- Run the installer on an unsupported `uname -s` / `uname -m` combination and confirm it fails
with a clear error message.
6 changes: 3 additions & 3 deletions elixir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Symphony stops the active agent for that issue and cleans up matching workspaces
- The `github` skill expects Symphony's `github_graphql` app-server tool for raw GitHub GraphQL
operations such as comment editing or project field updates.
5. Customize the copied `WORKFLOW.md` file for your project.
- Configure the GitHub Project owner + number.
- Configure the GitHub Project owner + number, or let the first-run guided bootstrap pick/create a project for you when starting without a `WORKFLOW.md`.
- Configure the Project field used as "status" (default: `Status`).
- Ensure the Project field values match your expected states (for example: `Todo`, `In Progress`,
`In Review`, `Merging`, `Done`, `Rework`).
Expand Down Expand Up @@ -90,7 +90,7 @@ Pass a custom workflow file path to `./bin/symphony` when starting the service:
./bin/symphony /path/to/custom/WORKFLOW.md
```

If no path is passed, Symphony defaults to `./WORKFLOW.md` and creates a default template there when the file is missing. Passing an explicit path still requires that file to exist.
If no path is passed, Symphony defaults to `./WORKFLOW.md`. When that file is missing, interactive terminals offer a guided GitHub Project bootstrap that can pick or create a ProjectV2 board and choose Codex defaults; non-interactive runs still create the baseline template automatically. Passing an explicit path still requires that file to exist.

Optional flags:

Expand Down Expand Up @@ -165,7 +165,7 @@ codex:
command: "$CODEX_BIN app-server --model gpt-5.3-codex"
```

- If the default `./WORKFLOW.md` is missing, Symphony creates a starter template on first run. Missing explicit workflow paths or invalid YAML still halt startup until fixed.
- If the default `./WORKFLOW.md` is missing, Symphony now offers a guided GitHub Project bootstrap on interactive TTYs and falls back to the baseline starter template in non-interactive runs. Missing explicit workflow paths or invalid YAML still halt startup until fixed.
- `server.port` or CLI `--port` enables the optional Phoenix LiveView dashboard and JSON API at
`/`, `/api/v1/state`, `/api/v1/<issue_identifier>`, and `/api/v1/refresh`.

Expand Down
33 changes: 28 additions & 5 deletions elixir/lib/symphony_elixir/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ defmodule SymphonyElixir.CLI do
@type ensure_started_result :: {:ok, [atom()]} | {:error, term()}
@type deps :: %{
file_regular?: (String.t() -> boolean()),
write_default_workflow: (String.t() -> :ok | {:error, term()}),
write_default_workflow: (String.t(), keyword() -> :ok | {:error, term()}),
interactive_stdio?: (-> boolean()),
notify: (String.t() -> term()),
set_workflow_file_path: (String.t() -> :ok | {:error, term()}),
set_logs_root: (String.t() -> :ok | {:error, term()}),
Expand Down Expand Up @@ -76,7 +77,8 @@ defmodule SymphonyElixir.CLI do
defp runtime_deps do
%{
file_regular?: &File.regular?/1,
write_default_workflow: &DefaultWorkflow.write/1,
write_default_workflow: &DefaultWorkflow.write/2,
interactive_stdio?: &interactive_stdio?/0,
notify: &IO.puts/1,
set_workflow_file_path: &SymphonyElixir.Workflow.set_workflow_file_path/1,
set_logs_root: &set_logs_root/1,
Expand All @@ -89,13 +91,15 @@ defmodule SymphonyElixir.CLI do
if deps.file_regular?.(path) do
:ok
else
case deps.write_default_workflow.(path) do
interactive? = deps.interactive_stdio?.()

case deps.write_default_workflow.(path, interactive: interactive?) do
:ok ->
deps.notify.(bootstrap_message(path))
:ok

{:error, reason} ->
{:error, "Failed to initialize workflow file #{path}: #{inspect(reason)}"}
format_bootstrap_error(path, reason)
end
end
end
Expand All @@ -108,8 +112,14 @@ defmodule SymphonyElixir.CLI do
end
end

defp format_bootstrap_error(_path, reason) when is_binary(reason), do: {:error, reason}

defp format_bootstrap_error(path, reason) do
{:error, "Failed to initialize workflow file #{path}: #{inspect(reason)}"}
end

defp bootstrap_message(path) do
"Created default WORKFLOW.md at #{path}. Update GITHUB_TOKEN, GITHUB_PROJECT_OWNER, GITHUB_PROJECT_NUMBER, SOURCE_REPO_URL, and SYMPHONY_WORKSPACE_ROOT for your repo."
"Created WORKFLOW.md at #{path}. Review the generated tracker, auth, and workspace settings before running Symphony."
end

defp maybe_set_logs_root(opts, deps) do
Expand Down Expand Up @@ -173,4 +183,17 @@ defmodule SymphonyElixir.CLI do
end
end
end

defp interactive_stdio? do
case System.find_executable("sh") do
nil ->
false

shell_path ->
case System.cmd(shell_path, ["-lc", "test -t 0 && test -t 1"], stderr_to_stdout: true) do
{_output, 0} -> true
_ -> false
end
end
end
end
9 changes: 5 additions & 4 deletions elixir/lib/symphony_elixir/config/github_auth.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule SymphonyElixir.Config.GitHubAuth do
@moduledoc false

@required_scopes ["project", "repo"]
@default_required_scopes ["project", "repo"]

@enforce_keys [:host, :source, :token]
defstruct [:host, :source, :token]
Expand All @@ -23,9 +23,10 @@ defmodule SymphonyElixir.Config.GitHubAuth do
@spec resolve_cli_token(String.t(), keyword()) :: {:ok, t()} | {:error, error_reason()}
def resolve_cli_token(host, opts \\ []) when is_binary(host) do
runner = Keyword.get(opts, :runner, &default_command_runner/3)
required_scopes = Keyword.get(opts, :required_scopes, @default_required_scopes)

with {:ok, account} <- fetch_active_account(host, runner),
:ok <- ensure_required_scopes(host, account),
:ok <- ensure_required_scopes(host, account, required_scopes),
{:ok, token_output} <- run_gh(host, gh_token_args(host), runner),
{:ok, token} <- parse_token(host, token_output) do
{:ok, %__MODULE__{host: host, source: :gh_cli, token: token}}
Expand Down Expand Up @@ -109,7 +110,7 @@ defmodule SymphonyElixir.Config.GitHubAuth do
end
end

defp ensure_required_scopes(host, account) do
defp ensure_required_scopes(host, account, required_scopes) when is_list(required_scopes) do
available_scopes = parse_scopes(Map.get(account, "scopes"))

case available_scopes do
Expand All @@ -120,7 +121,7 @@ defmodule SymphonyElixir.Config.GitHubAuth do
available_set = MapSet.new(scopes)

missing_scopes =
@required_scopes
required_scopes
|> Enum.reject(&MapSet.member?(available_set, &1))
|> Enum.sort()

Expand Down
31 changes: 27 additions & 4 deletions elixir/lib/symphony_elixir/default_workflow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,44 @@ defmodule SymphonyElixir.DefaultWorkflow do
Provides the default `WORKFLOW.md` template used for first-run bootstrap.
"""

alias SymphonyElixir.DefaultWorkflow.Bootstrap

@template_path Path.expand("../../priv/default_workflow.md", __DIR__)
@external_resource @template_path
@template_contents File.read!(@template_path)

@type write_option ::
{:interactive, boolean()}
| {:gets, (String.t() -> String.t() | nil)}
| {:puts, (String.t() -> term())}
| {:env_getter, (String.t() -> String.t() | nil)}
| {:github_cli_runner, (String.t(), [String.t()], keyword() -> term())}
| {:github_query_fun, (String.t(), map(), keyword() -> {:ok, map()} | {:error, term()})}
| {:github_endpoint, String.t()}

@type write_options :: [write_option()]

@spec contents() :: String.t()
def contents do
@template_contents
end

@spec write(Path.t()) :: :ok | {:error, term()}
def write(path) when is_binary(path) do
@spec bootstrap_contents(write_options()) :: {:ok, String.t()} | {:error, term()}
def bootstrap_contents(opts \\ []) do
if Keyword.get(opts, :interactive, false) do
Bootstrap.run(opts)
else
{:ok, contents()}
end
end

@spec write(Path.t(), write_options()) :: :ok | {:error, term()}
def write(path, opts \\ []) when is_binary(path) and is_list(opts) do
expanded_path = Path.expand(path)

with :ok <- File.mkdir_p(Path.dirname(expanded_path)) do
File.write(expanded_path, contents())
with {:ok, workflow_contents} <- bootstrap_contents(opts),
:ok <- File.mkdir_p(Path.dirname(expanded_path)) do
File.write(expanded_path, workflow_contents)
end
end
end
Loading