symphony-box 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cli.js +138 -0
  4. package/init.js +137 -0
  5. package/package.json +33 -0
  6. package/workflow.js +331 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Upstash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @upstash/symphony-box
2
+
3
+ CLI to set up [Symphony](https://github.com/odysseus0/symphony) (OpenAI Codex orchestrator) on an [Upstash Box](https://upstash.com/docs/box/overall/getstarted) for a GitHub repo connected to a Linear project.
4
+
5
+ ## What it does
6
+
7
+ 1. Checks your Linear project for required workflow states and creates any that are missing
8
+ 2. Creates an Upstash Box with your credentials
9
+ 3. Installs system dependencies, Codex, and mise on the box
10
+ 4. Clones your repo and writes a `WORKFLOW.md` config for Symphony
11
+ 5. Clones and builds Symphony on the box
12
+ 6. Starts Symphony
13
+
14
+ Once running, Symphony polls your Linear project and autonomously works on tickets using Codex.
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ npx symphony-box
20
+ ```
21
+
22
+ You will be prompted for:
23
+
24
+ - **Upstash Box API key** — from [console.upstash.com](https://console.upstash.com)
25
+ - **OpenAI API key** — used by Codex to work on tickets
26
+ - **Linear API key** — to poll and update tickets
27
+ - **GitHub token** — requires read/write access for contents and pull requests
28
+
29
+ Then select a GitHub repo and a Linear project from the list.
30
+
31
+ ## Flags
32
+
33
+ All prompts can be skipped via flags or environment variables:
34
+
35
+ | Flag | Env var | Description |
36
+ | ----------------------- | --------------------- | ------------------------------------ |
37
+ | `--upstash-box-api-key` | `UPSTASH_BOX_API_KEY` | Upstash Box API key |
38
+ | `--openai-api-key` | `OPENAI_API_KEY` | OpenAI API key |
39
+ | `--linear-api-key` | `LINEAR_API_KEY` | Linear API key |
40
+ | `--github-token` | `GITHUB_TOKEN` | GitHub token |
41
+ | `--repo-url` | `REPO_URL` | GitHub repo URL (skip selection) |
42
+ | `--project-name` | `LINEAR_PROJECT_NAME` | Linear project name (skip selection) |
43
+
44
+ ## Required Linear workflow states
45
+
46
+ Symphony requires these states to exist in your Linear team:
47
+
48
+ - **Rework** — reviewer requested changes
49
+ - **Human Review** — PR is ready for human approval
50
+ - **Merging** — approved; Symphony will merge the PR
51
+
52
+ The CLI will detect missing states and offer to create them.
53
+
54
+ ## License
55
+
56
+ MIT
package/cli.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import { intro, outro, text, select, confirm, isCancel, cancel, spinner, log } from "@clack/prompts";
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import { run_init } from "./init.js";
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name("symphony-box")
11
+ .description("Set up Symphony on an Upstash Box for a GitHub repo")
12
+ .option("--upstash-box-api-key <key>", "Upstash Box API key")
13
+ .option("--openai-api-key <key>", "OpenAI API key")
14
+ .option("--linear-api-key <key>", "Linear API key")
15
+ .option("--github-token <token>", "GitHub token")
16
+ .option("--repo-url <url>", "GitHub repo URL (skip selection)")
17
+ .option("--project-name <name>", "Linear project name (skip selection)")
18
+ .parse(process.argv);
19
+
20
+ const opts = program.opts();
21
+
22
+ function checkCancel(result) {
23
+ if (isCancel(result)) {
24
+ cancel("Cancelled.");
25
+ process.exit(0);
26
+ }
27
+ return result;
28
+ }
29
+
30
+ async function promptText(message, envKey, flagValue, placeholder) {
31
+ const value = flagValue ?? process.env[envKey];
32
+ if (value) return value;
33
+ return checkCancel(
34
+ await text({ message, placeholder, validate: (v) => (v ? undefined : "Required") })
35
+ );
36
+ }
37
+
38
+ async function pickRepo(githubToken) {
39
+ const fixed = opts.repoUrl ?? process.env.REPO_URL;
40
+ if (fixed) return { repoUrl: fixed, repoName: fixed.split("/").pop().replace(/\.git$/, "") };
41
+
42
+ const s = spinner();
43
+ s.start("Fetching your GitHub repos...");
44
+ const res = await fetch("https://api.github.com/user/repos?per_page=100&sort=updated", {
45
+ headers: { Authorization: `Bearer ${githubToken}`, Accept: "application/vnd.github+json" },
46
+ });
47
+ const repos = await res.json();
48
+ s.stop("Repos loaded");
49
+
50
+ const selected = checkCancel(
51
+ await select({
52
+ message: "Select a GitHub repo",
53
+ options: repos.map((r) => ({
54
+ value: { repoUrl: r.clone_url, repoName: r.name },
55
+ label: r.full_name,
56
+ hint: r.private ? "private" : "public",
57
+ })),
58
+ })
59
+ );
60
+ return selected;
61
+ }
62
+
63
+ async function pickLinearProject(linearApiKey) {
64
+ const fixed = opts.projectName ?? process.env.LINEAR_PROJECT_NAME;
65
+ if (fixed) return fixed;
66
+
67
+ const s = spinner();
68
+ s.start("Fetching your Linear projects...");
69
+ const res = await fetch("https://api.linear.app/graphql", {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json", Authorization: linearApiKey },
72
+ body: JSON.stringify({ query: "{ projects { nodes { name } } }" }),
73
+ });
74
+ const json = await res.json();
75
+ s.stop("Projects loaded");
76
+
77
+ return checkCancel(
78
+ await select({
79
+ message: "Select a Linear project",
80
+ options: json.data.projects.nodes.map((p) => ({ value: p.name, label: p.name })),
81
+ })
82
+ );
83
+ }
84
+
85
+ async function main() {
86
+ intro(chalk.cyan("Symphony Box Setup"));
87
+
88
+ const upstashBoxApiKey = await promptText("Upstash Box API key", "UPSTASH_BOX_API_KEY", opts.upstashBoxApiKey);
89
+
90
+ const openaiApiKey = await promptText("OpenAI API key", "OPENAI_API_KEY", opts.openaiApiKey, "sk-...");
91
+ const linearApiKey = await promptText("Linear API key", "LINEAR_API_KEY", opts.linearApiKey, "lin_api_...");
92
+ const githubToken = await promptText("GitHub token (requires read/write access for contents and pull requests)", "GITHUB_TOKEN", opts.githubToken, "ghp_...");
93
+
94
+ const { repoUrl, repoName } = await pickRepo(githubToken);
95
+ const linearProjectName = await pickLinearProject(linearApiKey);
96
+
97
+ const s = spinner();
98
+ s.start("Starting...");
99
+
100
+ try {
101
+ const { boxId, stream } = await run_init(
102
+ { upstashBoxApiKey, openaiApiKey, linearApiKey, githubToken, repoUrl, repoName, linearProjectName },
103
+ {
104
+ onStep: (_step, message) => s.message(message),
105
+ onMissingStates: async (missing) => {
106
+ s.stop();
107
+ const names = missing.map((s) => ` • ${s.name}`).join("\n");
108
+ const ok = checkCancel(
109
+ await confirm({
110
+ message: `These workflow states are missing and required by Symphony:\n${names}\n Create them?`,
111
+ })
112
+ );
113
+ s.start();
114
+ return ok;
115
+ },
116
+ }
117
+ );
118
+
119
+ s.stop("Done");
120
+
121
+ // Stream Symphony output until disconnected
122
+ try {
123
+ for await (const chunk of stream) {
124
+ if (chunk.type === "output") process.stdout.write(chunk.data);
125
+ }
126
+ } catch {
127
+ // Stream disconnected — Symphony keeps running on the box
128
+ }
129
+
130
+ outro(chalk.green("Symphony is running!") + `\n\n Box ID: ${chalk.bold(boxId)}`);
131
+ } catch (err) {
132
+ s.stop("Failed");
133
+ log.error(chalk.red(err.message));
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ main();
package/init.js ADDED
@@ -0,0 +1,137 @@
1
+ import { Box } from "@upstash/box";
2
+ import { buildWorkflow } from "./workflow.js";
3
+
4
+ const SYMPHONY_URL = "https://github.com/odysseus0/symphony";
5
+ const MISE = "/home/boxuser/.local/bin/mise";
6
+
7
+ async function linearQuery(linearApiKey, query, variables) {
8
+ const res = await fetch("https://api.linear.app/graphql", {
9
+ method: "POST",
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ Authorization: linearApiKey,
13
+ },
14
+ body: JSON.stringify({ query, variables }),
15
+ });
16
+ const json = await res.json();
17
+ if (json.errors) throw new Error(JSON.stringify(json.errors));
18
+ return json.data;
19
+ }
20
+
21
+ const REQUIRED_STATES = [
22
+ { name: "Rework", color: "#db6e1f" },
23
+ { name: "Human Review", color: "#da8b0d" },
24
+ { name: "Merging", color: "#0f783c" },
25
+ ];
26
+
27
+ async function setupLinear(linearApiKey, projectName, onMissingStates) {
28
+ const data = await linearQuery(
29
+ linearApiKey,
30
+ `{ projects { nodes { name slugId teams { nodes { id } } } } }`
31
+ );
32
+ const project = data.projects.nodes.find((p) => p.name === projectName);
33
+ if (!project) throw new Error(`Linear project "${projectName}" not found`);
34
+
35
+ const teamId = project.teams.nodes[0].id;
36
+
37
+ const statesData = await linearQuery(
38
+ linearApiKey,
39
+ `{ workflowStates { nodes { name team { id } } } }`
40
+ );
41
+ const existing = statesData.workflowStates.nodes
42
+ .filter((s) => s.team?.id === teamId)
43
+ .map((s) => s.name);
44
+
45
+ const missing = REQUIRED_STATES.filter((s) => !existing.includes(s.name));
46
+
47
+ if (missing.length > 0) {
48
+ const confirmed = await onMissingStates(missing);
49
+ if (!confirmed)
50
+ throw new Error("Aborted: required workflow states not created.");
51
+
52
+ for (const state of missing) {
53
+ await linearQuery(
54
+ linearApiKey,
55
+ `mutation($input: WorkflowStateCreateInput!) { workflowStateCreate(input: $input) { success } }`,
56
+ {
57
+ input: {
58
+ teamId,
59
+ name: state.name,
60
+ type: "started",
61
+ color: state.color,
62
+ },
63
+ }
64
+ );
65
+ }
66
+ }
67
+
68
+ return { slugId: project.slugId };
69
+ }
70
+
71
+ export async function run_init(
72
+ {
73
+ upstashBoxApiKey,
74
+ openaiApiKey,
75
+ linearApiKey,
76
+ githubToken,
77
+ repoUrl,
78
+ repoName,
79
+ linearProjectName,
80
+ },
81
+ { onStep = () => {}, onMissingStates = async () => true } = {}
82
+ ) {
83
+ onStep("linear", "Checking Linear project and workflow states...");
84
+ const { slugId } = await setupLinear(
85
+ linearApiKey,
86
+ linearProjectName,
87
+ onMissingStates
88
+ );
89
+
90
+ onStep("workflow", `Building WORKFLOW.md (slug: ${slugId})...`);
91
+ const workflow = buildWorkflow(slugId, repoUrl);
92
+
93
+ onStep("box", "Creating Upstash Box...");
94
+ const box = await Box.create({
95
+ apiKey: upstashBoxApiKey,
96
+ runtime: "node",
97
+ git: { token: githubToken },
98
+ env: { LINEAR_API_KEY: linearApiKey, OPENAI_API_KEY: openaiApiKey },
99
+ });
100
+
101
+ onStep("deps", `Box ${box.id} — installing system dependencies...`);
102
+ await box.exec.command(
103
+ "sudo apk add --no-cache git github-cli build-base perl bison ncurses-dev openssl-dev libssh-dev unixodbc-dev libxml2-dev"
104
+ );
105
+
106
+ onStep("codex", "Installing Codex...");
107
+ await box.exec.command("sudo npm install -g @openai/codex");
108
+
109
+ onStep("mise", "Installing mise...");
110
+ await box.exec.command("curl https://mise.run | sh");
111
+
112
+ onStep("auth", "Authenticating gh and Codex...");
113
+ await box.exec.command("gh auth setup-git");
114
+ await box.exec.command(`echo "${openaiApiKey}" | codex login --with-api-key`);
115
+
116
+ onStep("repo", `Cloning ${repoName} and writing WORKFLOW.md...`);
117
+ await box.exec.command(`git clone ${repoUrl}`);
118
+ await box.files.write({
119
+ path: `/workspace/home/${repoName}/WORKFLOW.md`,
120
+ content: workflow,
121
+ });
122
+
123
+ onStep("build", "Cloning and building Symphony (~5 mins)...");
124
+ await box.exec.command(`git clone ${SYMPHONY_URL}`);
125
+ await box.exec.command(
126
+ `cd symphony/elixir && ${MISE} trust && ${MISE} install`
127
+ );
128
+ await box.exec.command(`cd symphony/elixir && ${MISE} exec -- mix setup`);
129
+ await box.exec.command(`cd symphony/elixir && ${MISE} exec -- mix build`);
130
+
131
+ onStep("run", "Starting Symphony...");
132
+ const stream = await box.exec.stream(
133
+ `cd symphony/elixir && ${MISE} exec -- ./bin/symphony /workspace/home/${repoName}/WORKFLOW.md --i-understand-that-this-will-be-running-without-the-usual-guardrails`
134
+ );
135
+
136
+ return { boxId: box.id, stream };
137
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "symphony-box",
3
+ "version": "1.0.0",
4
+ "description": "CLI to set up Symphony (OpenAI Codex orchestrator) on an Upstash Box for a GitHub repo",
5
+ "type": "module",
6
+ "bin": {
7
+ "symphony-box": "./cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "init.js",
12
+ "workflow.js"
13
+ ],
14
+ "keywords": [
15
+ "upstash",
16
+ "box",
17
+ "symphony",
18
+ "codex",
19
+ "openai",
20
+ "cli",
21
+ "linear"
22
+ ],
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "dependencies": {
28
+ "@clack/prompts": "^0.11.0",
29
+ "@upstash/box": "latest",
30
+ "chalk": "^5.4.1",
31
+ "commander": "^14.0.0"
32
+ }
33
+ }
package/workflow.js ADDED
@@ -0,0 +1,331 @@
1
+ export function buildWorkflow(slugId, repoUrl) {
2
+ return `---
3
+ tracker:
4
+ kind: linear
5
+ project_slug: "${slugId}"
6
+ active_states:
7
+ - Todo
8
+ - In Progress
9
+ - Merging
10
+ - Rework
11
+ terminal_states:
12
+ - Closed
13
+ - Cancelled
14
+ - Canceled
15
+ - Duplicate
16
+ - Done
17
+ polling:
18
+ interval_ms: 5000
19
+ workspace:
20
+ root: ~/code/symphony-workspaces
21
+ hooks:
22
+ after_create: |
23
+ git clone --depth 1 ${repoUrl} .
24
+ before_remove: |
25
+ branch=$(git branch --show-current 2>/dev/null)
26
+ if [ -n "$branch" ] && command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
27
+ gh pr list --head "$branch" --state open --json number --jq '.[].number' | while read -r pr; do
28
+ [ -n "$pr" ] && gh pr close "$pr" --comment "Closing because the Linear issue for branch $branch entered a terminal state without merge."
29
+ done
30
+ fi
31
+ agent:
32
+ max_concurrent_agents: 10
33
+ max_turns: 20
34
+ codex:
35
+ command: codex --config shell_environment_policy.inherit=all --config model_reasoning_effort=xhigh --model gpt-5.3-codex app-server
36
+ approval_policy: never
37
+ thread_sandbox: danger-full-access
38
+ turn_sandbox_policy:
39
+ type: dangerFullAccess
40
+ ---
41
+
42
+ You are working on a Linear ticket \`{{ issue.identifier }}\`
43
+
44
+ {% if attempt %}
45
+ Continuation context:
46
+
47
+ - This is retry attempt #{{ attempt }} because the ticket is still in an active state.
48
+ - Resume from the current workspace state instead of restarting from scratch.
49
+ - Do not repeat already-completed investigation or validation unless needed for new code changes.
50
+ - Do not end the turn while the issue remains in an active state unless you are blocked by missing required permissions/secrets.
51
+ {% endif %}
52
+
53
+ Issue context:
54
+ Identifier: {{ issue.identifier }}
55
+ Title: {{ issue.title }}
56
+ Current status: {{ issue.state }}
57
+ Labels: {{ issue.labels }}
58
+ URL: {{ issue.url }}
59
+
60
+ Description:
61
+ {% if issue.description %}
62
+ {{ issue.description }}
63
+ {% else %}
64
+ No description provided.
65
+ {% endif %}
66
+
67
+ Instructions:
68
+
69
+ 1. This is an unattended orchestration session. Never ask a human to perform follow-up actions.
70
+ 2. Only stop early for a true blocker (missing required auth/permissions/secrets). If blocked, record it in the workpad and move the issue according to workflow.
71
+ 3. Final message must report completed actions and blockers only. Do not include "next steps for user".
72
+
73
+ Work only in the provided repository copy. Do not touch any other path.
74
+
75
+ ## Prerequisite: \`linear_graphql\` tool is available
76
+
77
+ The agent talks to Linear via the \`linear_graphql\` tool injected by Symphony's app-server. If it is not present, stop and ask the user to configure Linear. Do not use a Linear MCP server — it returns full JSON payloads that waste tokens. Use \`linear_graphql\` with narrowly scoped queries instead.
78
+
79
+ ## Default posture
80
+
81
+ - Start by determining the ticket's current status, then follow the matching flow for that status.
82
+ - Start every task by opening the tracking workpad comment and bringing it up to date before doing new implementation work.
83
+ - Spend extra effort up front on planning and verification design before implementation.
84
+ - Reproduce first: always confirm the current behavior/issue signal before changing code so the fix target is explicit.
85
+ - Keep ticket metadata current (state, checklist, acceptance criteria, links).
86
+ - Treat a single persistent Linear comment as the source of truth for progress.
87
+ - Use that single workpad comment for all progress and handoff notes; do not post separate "done"/summary comments.
88
+ - Treat any ticket-authored \`Validation\`, \`Test Plan\`, or \`Testing\` section as non-negotiable acceptance input: mirror it in the workpad and execute it before considering the work complete.
89
+ - When meaningful out-of-scope improvements are discovered during execution,
90
+ file a separate Linear issue instead of expanding scope. The follow-up issue
91
+ must include a clear title, description, and acceptance criteria, be placed in
92
+ \`Backlog\`, be assigned to the same project as the current issue, link the
93
+ current issue as \`related\`, and use \`blockedBy\` when the follow-up depends on
94
+ the current issue.
95
+ - Move status only when the matching quality bar is met.
96
+ - Operate autonomously end-to-end unless blocked by missing requirements, secrets, or permissions.
97
+ - Use the blocked-access escape hatch only for true external blockers (missing required tools/auth) after exhausting documented fallbacks.
98
+
99
+ ## Related skills
100
+
101
+ - \`linear\`: interact with Linear.
102
+ - \`commit\`: produce clean, logical commits during implementation.
103
+ - \`push\`: keep remote branch current and publish updates.
104
+ - \`pull\`: keep branch updated with latest \`origin/main\` before handoff.
105
+ - \`land\`: when ticket reaches \`Merging\`, use the \`land\` skill, which includes the merge loop.
106
+
107
+ ## Status map
108
+
109
+ - \`Backlog\` -> out of scope for this workflow; do not modify.
110
+ - \`Todo\` -> queued; immediately transition to \`In Progress\` before active work.
111
+ - Special case: if a PR is already attached, treat as feedback/rework loop (run full PR feedback sweep, address or explicitly push back, revalidate, return to \`Human Review\`).
112
+ - \`In Progress\` -> implementation actively underway.
113
+ - \`Human Review\` -> PR is attached and validated; waiting on human approval.
114
+ - \`Merging\` -> approved by human; execute the \`land\` skill flow (do not call \`gh pr merge\` directly).
115
+ - \`Rework\` -> reviewer requested changes; planning + implementation required.
116
+ - \`Done\` -> terminal state; no further action required.
117
+
118
+ ## Step 0: Determine current ticket state and route
119
+
120
+ 1. Fetch the issue by explicit ticket ID.
121
+ 2. Read the current state.
122
+ 3. Route to the matching flow:
123
+ - \`Backlog\` -> do not modify issue content/state; stop and wait for human to move it to \`Todo\`.
124
+ - \`Todo\` -> immediately move to \`In Progress\`, then ensure bootstrap workpad comment exists (create if missing), then start execution flow.
125
+ - If PR is already attached, start by reviewing all open PR comments and deciding required changes vs explicit pushback responses.
126
+ - \`In Progress\` -> continue execution flow from current scratchpad comment.
127
+ - \`Human Review\` -> wait and poll for decision/review updates.
128
+ - \`Merging\` -> on entry, use the \`land\` skill; do not call \`gh pr merge\` directly.
129
+ - \`Rework\` -> run rework flow.
130
+ - \`Done\` -> do nothing and shut down.
131
+ 4. Check whether a PR already exists for the current branch and whether it is closed.
132
+ - If a branch PR exists and is \`CLOSED\` or \`MERGED\`, treat prior branch work as non-reusable for this run.
133
+ - Create a fresh branch from \`origin/main\` and restart execution flow as a new attempt.
134
+ 5. For \`Todo\` tickets, do startup sequencing in this exact order:
135
+ - \`update_issue(..., state: "In Progress")\`
136
+ - find/create \`## Codex Workpad\` bootstrap comment
137
+ - only then begin analysis/planning/implementation work.
138
+ 6. Add a short comment if state and issue content are inconsistent, then proceed with the safest flow.
139
+
140
+ ## Step 1: Start/continue execution (Todo or In Progress)
141
+
142
+ 1. Find or create a single persistent scratchpad comment for the issue:
143
+ - Search existing comments for a marker header: \`## Codex Workpad\`.
144
+ - Ignore resolved comments while searching; only active/unresolved comments are eligible to be reused as the live workpad.
145
+ - If found, reuse that comment; do not create a new workpad comment.
146
+ - If not found, create one workpad comment and use it for all updates.
147
+ - Persist the workpad comment ID and only write progress updates to that ID.
148
+ 2. If arriving from \`Todo\`, do not delay on additional status transitions: the issue should already be \`In Progress\` before this step begins.
149
+ 3. Immediately reconcile the workpad before new edits:
150
+ - Check off items that are already done.
151
+ - Expand/fix the plan so it is comprehensive for current scope.
152
+ - Ensure \`Acceptance Criteria\` and \`Validation\` are current and still make sense for the task.
153
+ 4. Start work by writing/updating a hierarchical plan in the workpad comment.
154
+ 5. Ensure the workpad includes a compact environment stamp at the top as a code fence line:
155
+ - Format: \`<host>:<abs-workdir>@<short-sha>\`
156
+ - Example: \`devbox-01:/home/dev-user/code/symphony-workspaces/MT-32@7bdde33bc\`
157
+ - Do not include metadata already inferable from Linear issue fields (\`issue ID\`, \`status\`, \`branch\`, \`PR link\`).
158
+ 6. Add explicit acceptance criteria and TODOs in checklist form in the same comment.
159
+ - If changes are user-facing, include a UI walkthrough acceptance criterion that describes the end-to-end user path to validate.
160
+ - If changes touch app files or app behavior, add explicit app-specific flow checks to \`Acceptance Criteria\` in the workpad (for example: launch path, changed interaction path, and expected result path).
161
+ - If the ticket description/comment context includes \`Validation\`, \`Test Plan\`, or \`Testing\` sections, copy those requirements into the workpad \`Acceptance Criteria\` and \`Validation\` sections as required checkboxes (no optional downgrade).
162
+ 7. Run a principal-style self-review of the plan and refine it in the comment.
163
+ 8. Before implementing, capture a concrete reproduction signal and record it in the workpad \`Notes\` section (command/output, screenshot, or deterministic UI behavior).
164
+ 9. Run the \`pull\` skill to sync with latest \`origin/main\` before any code edits, then record the pull/sync result in the workpad \`Notes\`.
165
+ - Include a \`pull skill evidence\` note with:
166
+ - merge source(s),
167
+ - result (\`clean\` or \`conflicts resolved\`),
168
+ - resulting \`HEAD\` short SHA.
169
+ 10. Compact context and proceed to execution.
170
+
171
+ ## PR feedback sweep protocol (required)
172
+
173
+ When a ticket has an attached PR, run this protocol before moving to \`Human Review\`:
174
+
175
+ 1. Identify the PR number from issue links/attachments.
176
+ 2. Gather feedback from all channels:
177
+ - Top-level PR comments (\`gh pr view --comments\`).
178
+ - Inline review comments (\`gh api repos/<owner>/<repo>/pulls/<pr>/comments\`).
179
+ - Review summaries/states (\`gh pr view --json reviews\`).
180
+ 3. Treat every actionable reviewer comment (human or bot), including inline review comments, as blocking until one of these is true:
181
+ - code/test/docs updated to address it, or
182
+ - explicit, justified pushback reply is posted on that thread.
183
+ 4. Update the workpad plan/checklist to include each feedback item and its resolution status.
184
+ 5. Re-run validation after feedback-driven changes and push updates.
185
+ 6. Repeat this sweep until there are no outstanding actionable comments.
186
+
187
+ ## Blocked-access escape hatch (required behavior)
188
+
189
+ Use this only when completion is blocked by missing required tools or missing auth/permissions that cannot be resolved in-session.
190
+
191
+ - GitHub is **not** a valid blocker by default. Always try fallback strategies first (alternate remote/auth mode, then continue publish/review flow).
192
+ - Do not move to \`Human Review\` for GitHub access/auth until all fallback strategies have been attempted and documented in the workpad.
193
+ - If a non-GitHub required tool is missing, or required non-GitHub auth is unavailable, move the ticket to \`Human Review\` with a short blocker brief in the workpad that includes:
194
+ - what is missing,
195
+ - why it blocks required acceptance/validation,
196
+ - exact human action needed to unblock.
197
+ - Keep the brief concise and action-oriented; do not add extra top-level comments outside the workpad.
198
+
199
+ ## Step 2: Execution phase (Todo -> In Progress -> Human Review)
200
+
201
+ 1. Determine current repo state (\`branch\`, \`git status\`, \`HEAD\`) and verify the kickoff \`pull\` sync result is already recorded in the workpad before implementation continues.
202
+ 2. If current issue state is \`Todo\`, move it to \`In Progress\`; otherwise leave the current state unchanged.
203
+ 3. Load the existing workpad comment and treat it as the active execution checklist.
204
+ - Edit it liberally whenever reality changes (scope, risks, validation approach, discovered tasks).
205
+ 4. Implement against the hierarchical TODOs and keep the comment current:
206
+ - Check off completed items.
207
+ - Add newly discovered items in the appropriate section.
208
+ - Keep parent/child structure intact as scope evolves.
209
+ - Update the workpad immediately after each meaningful milestone (for example: reproduction complete, code change landed, validation run, review feedback addressed).
210
+ - Never leave completed work unchecked in the plan.
211
+ - For tickets that started as \`Todo\` with an attached PR, run the full PR feedback sweep protocol immediately after kickoff and before new feature work.
212
+ 5. Run validation/tests required for the scope.
213
+ - Mandatory gate: execute all ticket-provided \`Validation\`/\`Test Plan\`/\`Testing\` requirements when present; treat unmet items as incomplete work.
214
+ - Prefer a targeted proof that directly demonstrates the behavior you changed.
215
+ - You may make temporary local proof edits to validate assumptions (for example: tweak a local build input for \`make\`, or hardcode a UI account / response path) when this increases confidence.
216
+ - Revert every temporary proof edit before commit/push.
217
+ - Document these temporary proof steps and outcomes in the workpad \`Validation\`/\`Notes\` sections so reviewers can follow the evidence.
218
+ - If app-touching, run runtime validation and capture screenshots/recordings. Upload media to Linear using the \`linear\` skill's \`fileUpload\` flow and embed in the workpad comment.
219
+ 6. Re-check all acceptance criteria and close any gaps.
220
+ 7. Before every \`git push\` attempt, run the required validation for your scope and confirm it passes; if it fails, address issues and rerun until green, then commit and push changes.
221
+ 8. Attach PR URL to the issue (prefer attachment; use the workpad comment only if attachment is unavailable).
222
+ - Ensure the GitHub PR has label \`symphony\` (add it if missing).
223
+ 9. Merge latest \`origin/main\` into branch, resolve conflicts, and rerun checks.
224
+ 10. Update the workpad comment with final checklist status and validation notes.
225
+ - Mark completed plan/acceptance/validation checklist items as checked.
226
+ - Add final handoff notes (commit + validation summary) in the same workpad comment.
227
+ - Do not include PR URL in the workpad comment; keep PR linkage on the issue via attachment/link fields.
228
+ - Add a short \`### Confusions\` section at the bottom when any part of task execution was unclear/confusing, with concise bullets.
229
+ - Do not post any additional completion summary comment.
230
+ 11. Before moving to \`Human Review\`, poll PR feedback and checks:
231
+ - Read the PR \`Manual QA Plan\` comment (when present) and use it to sharpen UI/runtime test coverage for the current change.
232
+ - Run the full PR feedback sweep protocol.
233
+ - Confirm PR checks are passing (green) after the latest changes.
234
+ - Confirm every required ticket-provided validation/test-plan item is explicitly marked complete in the workpad.
235
+ - Repeat this check-address-verify loop until no outstanding comments remain and checks are fully passing.
236
+ - Re-open and refresh the workpad before state transition so \`Plan\`, \`Acceptance Criteria\`, and \`Validation\` exactly match completed work.
237
+ 12. Only then move issue to \`Human Review\`.
238
+ - Exception: if blocked by missing required non-GitHub tools/auth per the blocked-access escape hatch, move to \`Human Review\` with the blocker brief and explicit unblock actions.
239
+ 13. For \`Todo\` tickets that already had a PR attached at kickoff:
240
+ - Ensure all existing PR feedback was reviewed and resolved, including inline review comments (code changes or explicit, justified pushback response).
241
+ - Ensure branch was pushed with any required updates.
242
+ - Then move to \`Human Review\`.
243
+
244
+ ## Step 3: Human Review and merge handling
245
+
246
+ 1. When the issue is in \`Human Review\`, do not code or change ticket content.
247
+ 2. Poll for updates as needed, including GitHub PR review comments from humans and bots.
248
+ 3. If review feedback requires changes, move the issue to \`Rework\` and follow the rework flow.
249
+ 4. If approved, human moves the issue to \`Merging\`.
250
+ 5. When the issue is in \`Merging\`, use the \`land\` skill and run it in a loop until the PR is merged. Do not call \`gh pr merge\` directly.
251
+ 6. After merge is complete, move the issue to \`Done\`.
252
+
253
+ ## Step 4: Rework handling
254
+
255
+ 1. Treat \`Rework\` as a full approach reset, not incremental patching.
256
+ 2. Re-read the full issue body and all human comments; explicitly identify what will be done differently this attempt.
257
+ 3. Close the existing PR tied to the issue.
258
+ 4. Remove the existing \`## Codex Workpad\` comment from the issue.
259
+ 5. Create a fresh branch from \`origin/main\`.
260
+ 6. Start over from the normal kickoff flow:
261
+ - If current issue state is \`Todo\`, move it to \`In Progress\`; otherwise keep the current state.
262
+ - Create a new bootstrap \`## Codex Workpad\` comment.
263
+ - Build a fresh plan/checklist and execute end-to-end.
264
+
265
+ ## Completion bar before Human Review
266
+
267
+ - Step 1/2 checklist is fully complete and accurately reflected in the single workpad comment.
268
+ - Acceptance criteria and required ticket-provided validation items are complete.
269
+ - Validation/tests are green for the latest commit.
270
+ - PR feedback sweep is complete and no actionable comments remain.
271
+ - PR checks are green, branch is pushed, and PR is linked on the issue.
272
+ - Required PR metadata is present (\`symphony\` label).
273
+ - If app-touching, runtime validation is complete and media evidence is uploaded to the Linear workpad.
274
+
275
+ ## Guardrails
276
+
277
+ - If the branch PR is already closed/merged, do not reuse that branch or prior implementation state for continuation.
278
+ - For closed/merged branch PRs, create a new branch from \`origin/main\` and restart from reproduction/planning as if starting fresh.
279
+ - If issue state is \`Backlog\`, do not modify it; wait for human to move to \`Todo\`.
280
+ - Do not edit the issue body/description for planning or progress tracking.
281
+ - Use exactly one persistent workpad comment (\`## Codex Workpad\`) per issue.
282
+ - If comment editing is unavailable in-session, use the update script. Only report blocked if both \`linear_graphql\` editing and script-based editing are unavailable.
283
+ - Temporary proof edits are allowed only for local verification and must be reverted before commit.
284
+ - If out-of-scope improvements are found, create a separate Backlog issue rather
285
+ than expanding current scope, and include a clear
286
+ title/description/acceptance criteria, same-project assignment, a \`related\`
287
+ link to the current issue, and \`blockedBy\` when the follow-up depends on the
288
+ current issue.
289
+ - Do not move to \`Human Review\` unless the \`Completion bar before Human Review\` is satisfied.
290
+ - In \`Human Review\`, do not make changes; wait and poll.
291
+ - If state is terminal (\`Done\`), do nothing and shut down.
292
+ - Keep issue text concise, specific, and reviewer-oriented.
293
+ - If blocked and no workpad exists yet, add one blocker comment describing blocker, impact, and next unblock action.
294
+
295
+ ## Workpad template
296
+
297
+ Use this exact structure for the persistent workpad comment and keep it updated in place throughout execution:
298
+
299
+ \`\`\`\`md
300
+ ## Codex Workpad
301
+
302
+ \`\`\`text
303
+ <hostname>:<abs-path>@<short-sha>
304
+ \`\`\`
305
+
306
+ ### Plan
307
+
308
+ - [ ] 1\\. Parent task
309
+ - [ ] 1.1 Child task
310
+ - [ ] 1.2 Child task
311
+ - [ ] 2\\. Parent task
312
+
313
+ ### Acceptance Criteria
314
+
315
+ - [ ] Criterion 1
316
+ - [ ] Criterion 2
317
+
318
+ ### Validation
319
+
320
+ - [ ] targeted tests: \`<command>\`
321
+
322
+ ### Notes
323
+
324
+ - <short progress note with timestamp>
325
+
326
+ ### Confusions
327
+
328
+ - <only include when something was confusing during execution>
329
+ \`\`\`\`
330
+ `;
331
+ }