@gh-symphony/cli 0.0.19 → 0.0.21

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.
@@ -4,12 +4,18 @@ import {
4
4
  GitHubScopeError,
5
5
  checkRequiredScopes,
6
6
  createClient,
7
+ discoverUserProjects,
7
8
  getGhTokenWithSource,
8
9
  getProjectDetail,
9
10
  listUserProjects,
10
11
  resolveGitHubAuth,
11
12
  validateToken
12
- } from "./chunk-TILHWBP6.js";
13
+ } from "./chunk-C67H3OUL.js";
14
+ import {
15
+ formatClaudePreflightText,
16
+ resolveClaudeCommandBinary,
17
+ runClaudePreflight
18
+ } from "./chunk-QEONJ5DZ.js";
13
19
  import {
14
20
  loadGlobalConfig,
15
21
  saveGlobalConfig,
@@ -18,8 +24,9 @@ import {
18
24
 
19
25
  // src/commands/init.ts
20
26
  import * as p from "@clack/prompts";
27
+ import { spawnSync } from "child_process";
21
28
  import { createHash } from "crypto";
22
- import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
29
+ import { chmod, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
23
30
  import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
24
31
 
25
32
  // src/mapping/smart-defaults.ts
@@ -99,10 +106,243 @@ function validateStateMapping(mappings) {
99
106
  return { valid: errors.length === 0, errors, warnings };
100
107
  }
101
108
 
109
+ // src/prompts/runtime-claude-constraints.ts
110
+ var CLAUDE_RUNTIME_CONSTRAINTS_SECTION = `## Runtime Constraints
111
+
112
+ 1. This run uses \`claude-print\` (Claude Code CLI) in non-interactive mode via \`claude -p\`.
113
+ 2. Slash commands such as \`/commit\`, \`/push\`, \`/gh-project\`, \`/gh-pr-writeup\` are NOT available (CLI limitation, independent of isolation settings).
114
+ 3. Use \`gh\`, \`git\`, repository scripts, and configured MCP tools directly instead.
115
+ 4. If a required permission or tool is unavailable, post a blocker comment on the issue and exit. Do not wait for human input.`;
116
+ var CLAUDE_PERMISSIVE_ISOLATION_NOTE = "<!-- Runtime trade-off note: Permissive preset requires an isolated workspace. Symphony runs each issue in `.runtime/symphony-workspaces/<workspace-id>/`, a throwaway clone. If you disable workspace isolation or mount host paths into worker containers, do not use this runtime in production. -->";
117
+ var CLAUDE_ISOLATION_OFF_NOTE = "<!-- Runtime trade-off note: Isolation is off by default \u2014 the agent will pick up your `CLAUDE.md`, project skills, and personal MCPs from `~/.claude/`. Turn isolation on when running in multi-operator CI, shared infrastructure, or when reproducibility across machines matters. -->";
118
+ var CLAUDE_RUNTIME_PROMPT_PREAMBLE = [
119
+ CLAUDE_RUNTIME_CONSTRAINTS_SECTION,
120
+ CLAUDE_PERMISSIVE_ISOLATION_NOTE,
121
+ CLAUDE_ISOLATION_OFF_NOTE
122
+ ].join("\n\n");
123
+
124
+ // src/workflow/default-hooks.ts
125
+ var DEFAULT_AFTER_CREATE_HOOK_PATH = "hooks/after_create.sh";
126
+ var DEFAULT_AFTER_CREATE_HOOK_LABEL = "Workspace hook scaffold";
127
+ var DEFAULT_AFTER_CREATE_HOOK_COMMENT = "scaffolded by workflow init; customize this script for repository setup";
128
+ var DEFAULT_AFTER_CREATE_HOOK_CONTENT = `#!/usr/bin/env bash
129
+ set -euo pipefail
130
+
131
+ # Customize this hook to prepare a freshly created workspace.
132
+ # This scaffold is intentionally a no-op so generated workflows run cleanly.
133
+ exit 0
134
+ `;
135
+
136
+ // src/workflow/repository-guidance.ts
137
+ function normalizeCommand(command) {
138
+ return command.replace(/\s+/g, " ").trim();
139
+ }
140
+ function renderInlineCode(command) {
141
+ const normalized = normalizeCommand(command);
142
+ const longestBacktickRun = Math.max(
143
+ 0,
144
+ ...Array.from(normalized.matchAll(/`+/g), (match) => match[0].length)
145
+ );
146
+ const fence = "`".repeat(longestBacktickRun + 1);
147
+ const padded = normalized.startsWith("`") || normalized.endsWith("`") ? ` ${normalized} ` : normalized;
148
+ return `${fence}${padded}${fence}`;
149
+ }
150
+ function buildRunnableScriptCommand(packageManager, scriptName) {
151
+ switch (packageManager) {
152
+ case "pnpm":
153
+ return scriptName === "test" ? "pnpm test" : `pnpm ${scriptName}`;
154
+ case "npm":
155
+ return scriptName === "test" ? "npm test" : `npm run ${scriptName}`;
156
+ case "yarn":
157
+ return `yarn ${scriptName}`;
158
+ case "bun":
159
+ return `bun run ${scriptName}`;
160
+ default:
161
+ return null;
162
+ }
163
+ }
164
+ function formatDetectedCommand(label, rawCommand, packageManager) {
165
+ if (!rawCommand) {
166
+ return null;
167
+ }
168
+ const runnableCommand = buildRunnableScriptCommand(packageManager, label);
169
+ const normalizedRawCommand = normalizeCommand(rawCommand);
170
+ if (!runnableCommand) {
171
+ return `${label}: ${renderInlineCode(normalizedRawCommand)}`;
172
+ }
173
+ if (normalizeCommand(runnableCommand) === normalizedRawCommand) {
174
+ return `${label}: ${renderInlineCode(runnableCommand)}`;
175
+ }
176
+ return `${label}: ${renderInlineCode(runnableCommand)} (script: ${renderInlineCode(
177
+ normalizedRawCommand
178
+ )})`;
179
+ }
180
+ function buildRepositoryValidationGuidance(input) {
181
+ const lines = [];
182
+ const commands = [
183
+ formatDetectedCommand("test", input.testCommand, input.packageManager),
184
+ formatDetectedCommand("lint", input.lintCommand, input.packageManager),
185
+ formatDetectedCommand("build", input.buildCommand, input.packageManager)
186
+ ].filter((value) => value !== null);
187
+ if (commands.length > 0) {
188
+ lines.push(
189
+ `Detected repository validation commands: ${commands.join(" ; ")}.`
190
+ );
191
+ lines.push(
192
+ "Prefer these repository-defined commands over generic guesses when validating changes."
193
+ );
194
+ lines.push(
195
+ "Use the smallest relevant command during iteration, then run the full available validation sequence before handoff in this order when applicable: test, lint, build."
196
+ );
197
+ } else {
198
+ lines.push(
199
+ "No repository-specific test/lint/build scripts were detected. Keep the generic fallback posture: infer the smallest meaningful validation command from the files you changed, and explicitly report when no automated validation is available."
200
+ );
201
+ }
202
+ if (input.packageManager) {
203
+ lines.push(
204
+ `Use \`${input.packageManager}\` conventions for ad hoc install/run commands unless the repository clearly requires something else.`
205
+ );
206
+ }
207
+ if (input.monorepo) {
208
+ lines.push(
209
+ "This repository appears to be a monorepo. Infer the affected package or workspace first, prefer workspace-scoped validation when available, and avoid unnecessary full-repo runs unless cross-package changes require them."
210
+ );
211
+ }
212
+ return lines;
213
+ }
214
+
215
+ // src/workflow/workflow-runtime.ts
216
+ var CODEX_RUNTIME_TOKENS = ["codex-app-server", "codex"];
217
+ var CLAUDE_RUNTIME_TOKENS = ["claude-print", "claude-code"];
218
+ var DEFAULT_CODEX_APP_SERVER_ARGS = ["app-server"];
219
+ var DEFAULT_CLAUDE_PRINT_ARGS = [
220
+ "-p",
221
+ "--output-format",
222
+ "stream-json",
223
+ "--input-format",
224
+ "stream-json",
225
+ "--include-partial-messages",
226
+ "--verbose",
227
+ "--permission-mode",
228
+ "bypassPermissions"
229
+ ];
230
+ function normalizeInitRuntime(runtime) {
231
+ if (runtime === "codex") {
232
+ return "codex-app-server";
233
+ }
234
+ if (runtime === "claude-code") {
235
+ return "claude-print";
236
+ }
237
+ return runtime;
238
+ }
239
+ function isSupportedInitRuntime(runtime) {
240
+ const normalized = normalizeInitRuntime(runtime);
241
+ return normalized === "codex-app-server" || normalized === "claude-print";
242
+ }
243
+ function containsRuntimeToken(runtime, tokens) {
244
+ return tokens.some((token) => {
245
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
246
+ return new RegExp(`(^|[\\s"'\\\`])${escaped}(?=$|[\\s"'\\\`])`).test(
247
+ runtime
248
+ );
249
+ });
250
+ }
251
+ function isCodexRuntime(runtime) {
252
+ return normalizeInitRuntime(runtime) === "codex-app-server" || containsRuntimeToken(runtime, CODEX_RUNTIME_TOKENS);
253
+ }
254
+ function isClaudeRuntime(runtime) {
255
+ return normalizeInitRuntime(runtime) === "claude-print" || containsRuntimeToken(runtime, CLAUDE_RUNTIME_TOKENS);
256
+ }
257
+ function resolveRuntimeCommand(runtime) {
258
+ const normalized = normalizeInitRuntime(runtime);
259
+ if (normalized === "codex-app-server") {
260
+ return "codex";
261
+ }
262
+ if (normalized === "claude-print") {
263
+ return "claude";
264
+ }
265
+ return runtime;
266
+ }
267
+ function resolveRuntimeArgs(runtime) {
268
+ const normalized = normalizeInitRuntime(runtime);
269
+ if (normalized === "codex-app-server") {
270
+ return DEFAULT_CODEX_APP_SERVER_ARGS;
271
+ }
272
+ if (normalized === "claude-print") {
273
+ return DEFAULT_CLAUDE_PRINT_ARGS;
274
+ }
275
+ return [];
276
+ }
277
+ function resolveRuntimeAgentCommand(runtime) {
278
+ const command = resolveRuntimeCommand(runtime);
279
+ const args = resolveRuntimeArgs(runtime);
280
+ return args.length === 0 ? command : [command, ...args].join(" ");
281
+ }
282
+ function resolveShellAgentCommand(runtime) {
283
+ if (/^\s*bash\s+-lc\s+/.test(runtime)) {
284
+ return runtime;
285
+ }
286
+ const normalized = normalizeInitRuntime(runtime);
287
+ if (normalized === "codex-app-server" || normalized === "claude-print") {
288
+ return `bash -lc ${resolveRuntimeAgentCommand(normalized)}`;
289
+ }
290
+ return runtime;
291
+ }
292
+ function buildRuntimeFrontMatter(runtime) {
293
+ const normalized = normalizeInitRuntime(runtime);
294
+ if (normalized === "codex-app-server") {
295
+ return [
296
+ "runtime:",
297
+ " kind: codex-app-server",
298
+ " command: codex",
299
+ " args:",
300
+ " - app-server",
301
+ " isolation:",
302
+ " bare: false",
303
+ " strict_mcp_config: false",
304
+ " timeouts:",
305
+ " read_timeout_ms: 5000",
306
+ " turn_timeout_ms: 3600000",
307
+ " stall_timeout_ms: 300000"
308
+ ];
309
+ }
310
+ if (normalized === "claude-print") {
311
+ return [
312
+ "runtime:",
313
+ " kind: claude-print",
314
+ " command: claude",
315
+ " args:",
316
+ ...DEFAULT_CLAUDE_PRINT_ARGS.map((arg) => ` - ${arg}`),
317
+ " isolation:",
318
+ " bare: false",
319
+ " strict_mcp_config: false",
320
+ " auth:",
321
+ " env: ANTHROPIC_API_KEY",
322
+ " timeouts:",
323
+ " read_timeout_ms: 5000",
324
+ " turn_timeout_ms: 3600000",
325
+ " stall_timeout_ms: 900000"
326
+ ];
327
+ }
328
+ return [
329
+ "runtime:",
330
+ " kind: custom",
331
+ ` command: ${runtime}`,
332
+ " isolation:",
333
+ " bare: false",
334
+ " strict_mcp_config: false",
335
+ " timeouts:",
336
+ " read_timeout_ms: 5000",
337
+ " turn_timeout_ms: 3600000",
338
+ " stall_timeout_ms: 300000"
339
+ ];
340
+ }
341
+
102
342
  // src/workflow/generate-workflow-md.ts
103
343
  function generateWorkflowMarkdown(input) {
104
344
  const frontMatter = buildFrontMatter(input);
105
- const promptBody = buildPromptBody(input.mappings);
345
+ const promptBody = buildPromptBody(input);
106
346
  return `---
107
347
  ${frontMatter}---
108
348
  ${promptBody}
@@ -114,6 +354,9 @@ function buildFrontMatter(input) {
114
354
  lines.push(" kind: github-project");
115
355
  lines.push(` project_id: ${input.projectId}`);
116
356
  lines.push(` state_field: ${input.stateFieldName}`);
357
+ if (input.priorityFieldName) {
358
+ lines.push(` priority_field: ${input.priorityFieldName}`);
359
+ }
117
360
  if (input.lifecycle.activeStates.length > 0) {
118
361
  lines.push(" active_states:");
119
362
  for (const state of input.lifecycle.activeStates) {
@@ -132,36 +375,26 @@ function buildFrontMatter(input) {
132
375
  lines.push(` - ${state}`);
133
376
  }
134
377
  }
135
- const agentCommand = resolveAgentCommand(input.runtime);
136
378
  lines.push("polling:");
137
379
  lines.push(` interval_ms: ${input.pollIntervalMs ?? 3e4}`);
138
380
  lines.push("workspace:");
139
381
  lines.push(" root: .runtime/symphony-workspaces");
140
382
  lines.push("hooks:");
141
- lines.push(" after_create: hooks/after_create.sh");
383
+ lines.push(` after_create: ${DEFAULT_AFTER_CREATE_HOOK_PATH}`);
142
384
  lines.push("agent:");
143
385
  lines.push(" max_concurrent_agents: 10");
144
386
  lines.push(" max_retry_backoff_ms: 30000");
145
387
  lines.push(" retry_base_delay_ms: 10000");
146
- lines.push("codex:");
147
- lines.push(` command: ${agentCommand}`);
148
- lines.push(" read_timeout_ms: 5000");
149
- lines.push(" turn_timeout_ms: 3600000");
388
+ lines.push(...buildRuntimeFrontMatter(input.runtime));
150
389
  return lines.join("\n") + "\n";
151
390
  }
152
- function resolveAgentCommand(runtime) {
153
- switch (runtime) {
154
- case "codex":
155
- return "codex app-server";
156
- case "claude-code":
157
- return "claude-code";
158
- default:
159
- return runtime;
160
- }
161
- }
162
- function buildPromptBody(mappings) {
163
- const statusMap = generateStatusMapWithDescriptions(mappings);
164
- const template = `${statusMap}
391
+ function buildPromptBody(input) {
392
+ const statusMap = generateStatusMapWithDescriptions(input.mappings);
393
+ const validationGuidance = buildRepositoryValidationGuidance(
394
+ input.detectedEnvironment
395
+ );
396
+ const runtimePreamble = buildRuntimePromptPreamble(input.runtime);
397
+ const templateBody = `${statusMap}
165
398
 
166
399
  ## Agent Instructions
167
400
 
@@ -180,6 +413,10 @@ You are an AI coding agent working on issue {{issue.identifier}}: "{{issue.title
180
413
  2. Only abort early if there is a genuine blocker (missing required credentials or secrets).
181
414
  3. In your final message, report only what was completed and any blockers. Do not include "next steps".
182
415
 
416
+ ### Repository Validation Guidance
417
+
418
+ ${validationGuidance.map((line, index) => `${index + 1}. ${line}`).join("\n")}
419
+
183
420
  ### Workflow
184
421
 
185
422
  1. Read the issue description and understand the requirements.
@@ -218,7 +455,10 @@ Create a workpad comment on the issue with the following structure to track prog
218
455
 
219
456
  - Progress notes
220
457
  \`\`\``;
221
- return template;
458
+ return [runtimePreamble, templateBody].filter(Boolean).join("\n\n");
459
+ }
460
+ function buildRuntimePromptPreamble(runtime) {
461
+ return isClaudeRuntime(runtime) ? CLAUDE_RUNTIME_PROMPT_PREAMBLE : "";
222
462
  }
223
463
  function generateStatusMapWithDescriptions(mappings) {
224
464
  const roleDescriptions = {
@@ -237,7 +477,7 @@ function generateStatusMapWithDescriptions(mappings) {
237
477
  }
238
478
 
239
479
  // src/detection/environment-detector.ts
240
- import { access, readFile } from "fs/promises";
480
+ import { access, readFile, readdir } from "fs/promises";
241
481
  import { join } from "path";
242
482
  function isFileMissing(error) {
243
483
  return Boolean(
@@ -269,13 +509,25 @@ async function readJsonFile(path) {
269
509
  throw error;
270
510
  }
271
511
  }
512
+ async function readTextFile(path) {
513
+ try {
514
+ return await readFile(path, "utf8");
515
+ } catch (error) {
516
+ if (isFileMissing(error)) {
517
+ return null;
518
+ }
519
+ throw error;
520
+ }
521
+ }
272
522
  async function detectPackageManager(cwd) {
273
523
  const lockfiles = [
274
524
  { name: "pnpm-lock.yaml", manager: "pnpm" },
275
525
  { name: "bun.lock", manager: "bun" },
276
526
  { name: "bun.lockb", manager: "bun" },
277
527
  { name: "yarn.lock", manager: "yarn" },
278
- { name: "package-lock.json", manager: "npm" }
528
+ { name: "package-lock.json", manager: "npm" },
529
+ { name: "uv.lock", manager: "uv" },
530
+ { name: "poetry.lock", manager: "poetry" }
279
531
  ];
280
532
  for (const { name, manager } of lockfiles) {
281
533
  const exists = await fileExists(join(cwd, name));
@@ -285,7 +537,32 @@ async function detectPackageManager(cwd) {
285
537
  }
286
538
  return { packageManager: null, lockfile: null };
287
539
  }
288
- async function detectScripts(cwd) {
540
+ function normalizeCommand2(command) {
541
+ return command.replace(/\s+/g, " ").trim();
542
+ }
543
+ function addCandidate(candidates, label, command, priority) {
544
+ if (!command) {
545
+ return;
546
+ }
547
+ candidates[label].push({
548
+ command: normalizeCommand2(command),
549
+ priority
550
+ });
551
+ }
552
+ function resolveCommand(candidates, label) {
553
+ const entries = candidates[label];
554
+ if (entries.length === 0) {
555
+ return null;
556
+ }
557
+ const highestPriority = Math.max(...entries.map((entry) => entry.priority));
558
+ const highestPriorityCommands = [
559
+ ...new Set(
560
+ entries.filter((entry) => entry.priority === highestPriority).map((entry) => entry.command)
561
+ )
562
+ ];
563
+ return highestPriorityCommands.length === 1 ? highestPriorityCommands[0] : null;
564
+ }
565
+ async function detectNodeScripts(cwd) {
289
566
  const packageJson = await readJsonFile(join(cwd, "package.json"));
290
567
  if (!packageJson?.scripts) {
291
568
  return { testCommand: null, buildCommand: null, lintCommand: null };
@@ -296,6 +573,117 @@ async function detectScripts(cwd) {
296
573
  lintCommand: packageJson.scripts.lint ?? null
297
574
  };
298
575
  }
576
+ async function detectMakeCommands(cwd) {
577
+ const makefile = await readTextFile(join(cwd, "Makefile")) ?? await readTextFile(join(cwd, "makefile"));
578
+ if (!makefile) {
579
+ return { testCommand: null, buildCommand: null, lintCommand: null };
580
+ }
581
+ const hasTarget = (target) => new RegExp(`^${target}\\s*::?(?:\\s|$)`, "m").test(makefile);
582
+ return {
583
+ testCommand: hasTarget("test") ? "make test" : null,
584
+ lintCommand: hasTarget("lint") ? "make lint" : null,
585
+ buildCommand: hasTarget("build") ? "make build" : null
586
+ };
587
+ }
588
+ async function detectJustCommands(cwd) {
589
+ const justfile = await readTextFile(join(cwd, "justfile")) ?? await readTextFile(join(cwd, ".justfile"));
590
+ if (!justfile) {
591
+ return { testCommand: null, buildCommand: null, lintCommand: null };
592
+ }
593
+ const hasRecipe = (name) => new RegExp(`^${name}\\s*:(?!=)`, "m").test(justfile);
594
+ return {
595
+ testCommand: hasRecipe("test") ? "just test" : null,
596
+ lintCommand: hasRecipe("lint") ? "just lint" : null,
597
+ buildCommand: hasRecipe("build") ? "just build" : null
598
+ };
599
+ }
600
+ async function hasRequirementsFile(cwd) {
601
+ try {
602
+ const entries = await readdir(cwd);
603
+ return entries.some((entry) => /^requirements[^/]*\.txt$/i.test(entry));
604
+ } catch (error) {
605
+ if (isFileMissing(error)) {
606
+ return false;
607
+ }
608
+ throw error;
609
+ }
610
+ }
611
+ async function detectPythonCommands(cwd) {
612
+ const [
613
+ pyproject,
614
+ hasUvLock,
615
+ hasPoetryLock,
616
+ hasPytestIni,
617
+ hasToxIni,
618
+ hasRequirements
619
+ ] = await Promise.all([
620
+ readTextFile(join(cwd, "pyproject.toml")),
621
+ fileExists(join(cwd, "uv.lock")),
622
+ fileExists(join(cwd, "poetry.lock")),
623
+ fileExists(join(cwd, "pytest.ini")),
624
+ fileExists(join(cwd, "tox.ini")),
625
+ hasRequirementsFile(cwd)
626
+ ]);
627
+ const hasPythonSignals = pyproject !== null || hasUvLock || hasPoetryLock || hasPytestIni || hasToxIni || hasRequirements;
628
+ if (!hasPythonSignals) {
629
+ return { testCommand: null, buildCommand: null, lintCommand: null };
630
+ }
631
+ const hasPytestConfig = hasPytestIni || /\[tool\.pytest(?:\.ini_options)?\]/.test(pyproject ?? "");
632
+ if (!hasPytestConfig) {
633
+ return { testCommand: null, buildCommand: null, lintCommand: null };
634
+ }
635
+ const testCommand = hasUvLock ? "uv run pytest" : hasPoetryLock ? "poetry run pytest" : "pytest";
636
+ return { testCommand, buildCommand: null, lintCommand: null };
637
+ }
638
+ async function detectGoCommands(cwd) {
639
+ const hasGoMod = await fileExists(join(cwd, "go.mod"));
640
+ return {
641
+ testCommand: hasGoMod ? "go test ./..." : null,
642
+ buildCommand: null,
643
+ lintCommand: null
644
+ };
645
+ }
646
+ async function detectRustCommands(cwd) {
647
+ const hasCargoToml = await fileExists(join(cwd, "Cargo.toml"));
648
+ return {
649
+ testCommand: hasCargoToml ? "cargo test" : null,
650
+ buildCommand: null,
651
+ lintCommand: null
652
+ };
653
+ }
654
+ async function detectValidationCommands(cwd) {
655
+ const [makeCommands, justCommands, nodeCommands, pythonCommands, goCommands, rustCommands] = await Promise.all([
656
+ detectMakeCommands(cwd),
657
+ detectJustCommands(cwd),
658
+ detectNodeScripts(cwd),
659
+ detectPythonCommands(cwd),
660
+ detectGoCommands(cwd),
661
+ detectRustCommands(cwd)
662
+ ]);
663
+ const candidates = {
664
+ testCommand: [],
665
+ lintCommand: [],
666
+ buildCommand: []
667
+ };
668
+ for (const commandSet of [makeCommands, justCommands]) {
669
+ addCandidate(candidates, "testCommand", commandSet.testCommand, 3);
670
+ addCandidate(candidates, "lintCommand", commandSet.lintCommand, 3);
671
+ addCandidate(candidates, "buildCommand", commandSet.buildCommand, 3);
672
+ }
673
+ addCandidate(candidates, "testCommand", nodeCommands.testCommand, 2);
674
+ addCandidate(candidates, "lintCommand", nodeCommands.lintCommand, 2);
675
+ addCandidate(candidates, "buildCommand", nodeCommands.buildCommand, 2);
676
+ for (const commandSet of [pythonCommands, goCommands, rustCommands]) {
677
+ addCandidate(candidates, "testCommand", commandSet.testCommand, 1);
678
+ addCandidate(candidates, "lintCommand", commandSet.lintCommand, 1);
679
+ addCandidate(candidates, "buildCommand", commandSet.buildCommand, 1);
680
+ }
681
+ return {
682
+ testCommand: resolveCommand(candidates, "testCommand"),
683
+ lintCommand: resolveCommand(candidates, "lintCommand"),
684
+ buildCommand: resolveCommand(candidates, "buildCommand")
685
+ };
686
+ }
299
687
  async function detectCiPlatform(cwd) {
300
688
  const workflowsDir = join(cwd, ".github", "workflows");
301
689
  const exists = await fileExists(workflowsDir);
@@ -310,10 +698,18 @@ async function detectMonorepo(cwd) {
310
698
  if (hasLerna) {
311
699
  return true;
312
700
  }
701
+ const hasGoWorkspace = await fileExists(join(cwd, "go.work"));
702
+ if (hasGoWorkspace) {
703
+ return true;
704
+ }
313
705
  const packageJson = await readJsonFile(join(cwd, "package.json"));
314
706
  if (packageJson?.workspaces) {
315
707
  return true;
316
708
  }
709
+ const cargoToml = await readTextFile(join(cwd, "Cargo.toml"));
710
+ if (cargoToml?.includes("[workspace]")) {
711
+ return true;
712
+ }
317
713
  return false;
318
714
  }
319
715
  async function detectExistingSkills(cwd) {
@@ -357,7 +753,7 @@ async function detectEnvironment(cwd) {
357
753
  existingSkills
358
754
  ] = await Promise.all([
359
755
  detectPackageManager(cwd),
360
- detectScripts(cwd),
756
+ detectValidationCommands(cwd),
361
757
  detectCiPlatform(cwd),
362
758
  detectMonorepo(cwd),
363
759
  detectExistingSkills(cwd)
@@ -528,6 +924,11 @@ function generateReferenceWorkflow(input) {
528
924
  lines.push(" kind: github-project");
529
925
  lines.push(` project_id: ${input.projectId}`);
530
926
  lines.push(" state_field: Status");
927
+ if (input.priorityFieldName) {
928
+ lines.push(` priority_field: ${input.priorityFieldName}`);
929
+ } else {
930
+ lines.push(" # priority_field: Priority");
931
+ }
531
932
  lines.push("");
532
933
  const activeColumns = input.statusColumns.filter((c) => c.role === "active");
533
934
  const waitColumns = input.statusColumns.filter((c) => c.role === "wait");
@@ -558,8 +959,6 @@ function generateReferenceWorkflow(input) {
558
959
  lines.push(" blocker_check_states: [{first active state}]");
559
960
  }
560
961
  lines.push("");
561
- const agentCommand = resolveAgentCommand2(input.runtime);
562
- const hookComment = resolveHookComment(input.runtime);
563
962
  lines.push("polling:");
564
963
  lines.push(" interval_ms: 30000");
565
964
  lines.push("");
@@ -567,7 +966,9 @@ function generateReferenceWorkflow(input) {
567
966
  lines.push(" root: .runtime/symphony-workspaces");
568
967
  lines.push("");
569
968
  lines.push("hooks:");
570
- lines.push(` after_create: hooks/after_create.sh # ${hookComment}`);
969
+ lines.push(
970
+ ` after_create: ${DEFAULT_AFTER_CREATE_HOOK_PATH} # ${DEFAULT_AFTER_CREATE_HOOK_COMMENT}`
971
+ );
571
972
  lines.push(" before_run: null");
572
973
  lines.push(" after_run: null");
573
974
  lines.push(" before_remove: null");
@@ -579,11 +980,7 @@ function generateReferenceWorkflow(input) {
579
980
  lines.push(" retry_base_delay_ms: 10000");
580
981
  lines.push(" max_turns: 20");
581
982
  lines.push("");
582
- lines.push("codex:");
583
- lines.push(` command: ${agentCommand}`);
584
- lines.push(" read_timeout_ms: 5000");
585
- lines.push(" turn_timeout_ms: 3600000");
586
- lines.push(" stall_timeout_ms: 300000");
983
+ lines.push(...buildRuntimeFrontMatter(input.runtime));
587
984
  lines.push("");
588
985
  lines.push("---");
589
986
  lines.push("");
@@ -611,6 +1008,14 @@ function generateReferenceWorkflow(input) {
611
1008
  }
612
1009
  }
613
1010
  lines.push("");
1011
+ lines.push("## Repository Validation Guidance");
1012
+ lines.push("");
1013
+ for (const line of buildRepositoryValidationGuidance(
1014
+ input.detectedEnvironment
1015
+ )) {
1016
+ lines.push(`- ${line}`);
1017
+ }
1018
+ lines.push("");
614
1019
  lines.push("## Default Posture");
615
1020
  lines.push("");
616
1021
  lines.push(
@@ -812,26 +1217,6 @@ function generateReferenceWorkflow(input) {
812
1217
  lines.push("");
813
1218
  return lines.join("\n");
814
1219
  }
815
- function resolveAgentCommand2(runtime) {
816
- switch (runtime) {
817
- case "codex":
818
- return "codex app-server";
819
- case "claude-code":
820
- return "claude-code";
821
- default:
822
- return runtime;
823
- }
824
- }
825
- function resolveHookComment(runtime) {
826
- switch (runtime) {
827
- case "codex":
828
- return "npm/yarn/pnpm install script";
829
- case "claude-code":
830
- return "npm/yarn/pnpm install script";
831
- default:
832
- return "package-manager-specific script";
833
- }
834
- }
835
1220
  function resolveRoleAction(role) {
836
1221
  switch (role) {
837
1222
  case "active":
@@ -849,10 +1234,10 @@ function resolveRoleAction(role) {
849
1234
  import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
850
1235
  import { join as join2 } from "path";
851
1236
  function normalizeRuntimeForSkills(runtime) {
852
- if (runtime === "claude-code" || runtime.includes("claude-code")) {
1237
+ if (isClaudeRuntime(runtime)) {
853
1238
  return "claude-code";
854
1239
  }
855
- if (runtime === "codex" || runtime.includes("codex")) {
1240
+ if (isCodexRuntime(runtime)) {
856
1241
  return "codex";
857
1242
  }
858
1243
  return null;
@@ -921,6 +1306,17 @@ function generateGhSymphonySkill(ctx) {
921
1306
  );
922
1307
  lines.push("- `gh` CLI must be authenticated");
923
1308
  lines.push("");
1309
+ lines.push("## Repository Validation Guidance");
1310
+ lines.push("");
1311
+ lines.push(
1312
+ "Carry the detected repository validation posture into any new or refined `WORKFLOW.md` instead of falling back to generic instructions."
1313
+ );
1314
+ for (const line of buildRepositoryValidationGuidance(
1315
+ ctx.detectedEnvironment
1316
+ )) {
1317
+ lines.push(`- ${line}`);
1318
+ }
1319
+ lines.push("");
924
1320
  lines.push("## Mode Detection");
925
1321
  lines.push("");
926
1322
  lines.push("Check if `WORKFLOW.md` exists in the current directory:");
@@ -1406,6 +1802,15 @@ Then re-run: ${retryCommand}`,
1406
1802
  "Fix missing scope"
1407
1803
  );
1408
1804
  }
1805
+ function warnIfProjectDiscoveryPartial(result) {
1806
+ if (!result.partial) {
1807
+ return;
1808
+ }
1809
+ const limitDetail = result.reason === "result_limit" ? "the discovered project count reached the safety cap" : "the GitHub API request budget reached the safety cap";
1810
+ p.log.warn(
1811
+ `Project discovery may be incomplete: ${limitDetail}. Showing ${result.projects.length} discovered project${result.projects.length === 1 ? "" : "s"} after ${result.requests} request${result.requests === 1 ? "" : "s"}.`
1812
+ );
1813
+ }
1409
1814
  async function abortIfCancelled(input) {
1410
1815
  const result = await input;
1411
1816
  if (p.isCancel(result)) {
@@ -1418,6 +1823,7 @@ function parseInitFlags(args) {
1418
1823
  const flags = {
1419
1824
  dryRun: false,
1420
1825
  nonInteractive: false,
1826
+ runtime: "codex",
1421
1827
  skipSkills: false,
1422
1828
  skipContext: false
1423
1829
  };
@@ -1439,6 +1845,10 @@ function parseInitFlags(args) {
1439
1845
  flags.output = next;
1440
1846
  i += 1;
1441
1847
  break;
1848
+ case "--runtime":
1849
+ flags.runtime = next ?? "";
1850
+ i += 1;
1851
+ break;
1442
1852
  case "--skip-skills":
1443
1853
  flags.skipSkills = true;
1444
1854
  break;
@@ -1449,6 +1859,25 @@ function parseInitFlags(args) {
1449
1859
  }
1450
1860
  return flags;
1451
1861
  }
1862
+ async function runInitRuntimePreflight(runtime) {
1863
+ if (!isClaudeRuntime(runtime)) {
1864
+ return true;
1865
+ }
1866
+ const hasGitHubGraphqlToken = typeof process.env.GITHUB_GRAPHQL_TOKEN === "string" && process.env.GITHUB_GRAPHQL_TOKEN.trim().length > 0;
1867
+ const report = await runClaudePreflight({
1868
+ cwd: process.cwd(),
1869
+ env: process.env,
1870
+ command: resolveClaudeCommandBinary(resolveRuntimeCommand(runtime)) ?? resolveRuntimeCommand(runtime),
1871
+ includeGhAuth: !hasGitHubGraphqlToken
1872
+ });
1873
+ const message = formatClaudePreflightText(report);
1874
+ if (report.ok) {
1875
+ p.log.info(message);
1876
+ return true;
1877
+ }
1878
+ p.log.error(message);
1879
+ return false;
1880
+ }
1452
1881
  var handler = async (args, options) => {
1453
1882
  const flags = parseInitFlags(args);
1454
1883
  if (flags.nonInteractive) {
@@ -1458,6 +1887,71 @@ var handler = async (args, options) => {
1458
1887
  await runInteractive(flags, options);
1459
1888
  };
1460
1889
  var init_default = handler;
1890
+ function resolveInitRuntime(runtime) {
1891
+ return normalizeInitRuntime(runtime ?? "codex-app-server");
1892
+ }
1893
+ function validateInitRuntime(runtime) {
1894
+ if (isSupportedInitRuntime(runtime)) {
1895
+ return null;
1896
+ }
1897
+ return `Unsupported runtime '${runtime}'. Choose one of: codex-app-server, claude-print.`;
1898
+ }
1899
+ async function promptRuntimeSelection() {
1900
+ return abortIfCancelled(
1901
+ p.select({
1902
+ message: "Step 1/3 \u2014 Select the agent runtime:",
1903
+ options: [
1904
+ {
1905
+ value: "codex-app-server",
1906
+ label: "codex-app-server",
1907
+ hint: "Codex app-server JSON-RPC runtime"
1908
+ },
1909
+ {
1910
+ value: "claude-print",
1911
+ label: "claude-print",
1912
+ hint: "Claude Code non-interactive stream-json runtime"
1913
+ }
1914
+ ]
1915
+ })
1916
+ );
1917
+ }
1918
+ function commandRuns(binary, args) {
1919
+ const result = spawnSync(binary, args, {
1920
+ encoding: "utf8",
1921
+ stdio: ["ignore", "pipe", "pipe"],
1922
+ timeout: 3e3
1923
+ });
1924
+ return result.error === void 0 && result.status === 0;
1925
+ }
1926
+ function runRuntimePreflight(runtime) {
1927
+ const command = resolveRuntimeCommand(runtime);
1928
+ const checks = [];
1929
+ const versionOk = commandRuns(command, ["--version"]);
1930
+ checks.push({
1931
+ label: "Runtime binary",
1932
+ ok: versionOk,
1933
+ detail: versionOk ? `${command} is available on PATH.` : `${command} was not found or did not run with --version.`
1934
+ });
1935
+ if (isClaudeRuntime(runtime)) {
1936
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
1937
+ checks.push({
1938
+ label: "Claude auth",
1939
+ ok: hasAnthropicKey,
1940
+ detail: hasAnthropicKey ? "ANTHROPIC_API_KEY is set." : "ANTHROPIC_API_KEY is not set. Set it before running Claude workers."
1941
+ });
1942
+ }
1943
+ const okIcon = "OK";
1944
+ const failIcon = "FAIL";
1945
+ const lines = checks.map(
1946
+ (check) => `${check.ok ? okIcon : failIcon} ${check.label.padEnd(16)} ${check.detail}`
1947
+ );
1948
+ p.note(lines.join("\n"), `Runtime preflight \u2014 ${runtime}`);
1949
+ if (checks.some((check) => !check.ok)) {
1950
+ p.log.warn(
1951
+ "Runtime preflight found missing local prerequisites. Generated files still include the selected runtime defaults."
1952
+ );
1953
+ }
1954
+ }
1461
1955
  async function resolveChangeStatus(path, content, mode) {
1462
1956
  try {
1463
1957
  const existing = await readFile3(path, "utf8");
@@ -1487,6 +1981,9 @@ async function writePlannedFile(file) {
1487
1981
  const temporaryPath = `${file.path}.tmp`;
1488
1982
  await writeFile3(temporaryPath, file.content, "utf8");
1489
1983
  await rename2(temporaryPath, file.path);
1984
+ if (file.executable) {
1985
+ await chmod(file.path, 493);
1986
+ }
1490
1987
  return true;
1491
1988
  }
1492
1989
  function resolveStatusField(projectDetail) {
@@ -1494,13 +1991,68 @@ function resolveStatusField(projectDetail) {
1494
1991
  }
1495
1992
  function buildAutomaticStateMappings(statusField) {
1496
1993
  const mappings = {};
1497
- for (const mapping of inferAllStateRoles(statusField.options.map((o) => o.name))) {
1994
+ for (const mapping of inferAllStateRoles(
1995
+ statusField.options.map((o) => o.name)
1996
+ )) {
1498
1997
  if (mapping.role) {
1499
1998
  mappings[mapping.columnName] = { role: mapping.role };
1500
1999
  }
1501
2000
  }
1502
2001
  return mappings;
1503
2002
  }
2003
+ function isPriorityFieldCandidateName(fieldName) {
2004
+ return /\bpriority\b/i.test(fieldName.trim());
2005
+ }
2006
+ function resolvePriorityField(projectDetail, statusField) {
2007
+ const singleSelectFields = projectDetail.statusFields.filter(
2008
+ (field) => field.id !== statusField.id
2009
+ );
2010
+ const exactMatches = singleSelectFields.filter(
2011
+ (field) => field.name.trim().toLowerCase() === "priority"
2012
+ );
2013
+ if (exactMatches.length === 1) {
2014
+ return { field: exactMatches[0], ambiguous: [] };
2015
+ }
2016
+ if (exactMatches.length > 1) {
2017
+ return { field: null, ambiguous: exactMatches };
2018
+ }
2019
+ const likelyMatches = singleSelectFields.filter(
2020
+ (field) => isPriorityFieldCandidateName(field.name)
2021
+ );
2022
+ if (likelyMatches.length === 1) {
2023
+ return { field: likelyMatches[0], ambiguous: [] };
2024
+ }
2025
+ if (likelyMatches.length > 1) {
2026
+ return { field: null, ambiguous: likelyMatches };
2027
+ }
2028
+ return { field: null, ambiguous: [] };
2029
+ }
2030
+ async function promptPriorityField(priorityCandidates, options) {
2031
+ if (priorityCandidates.length === 0) {
2032
+ return null;
2033
+ }
2034
+ const selectedFieldId = await abortIfCancelled(
2035
+ p.select({
2036
+ message: `${options?.stepLabel ?? "Priority field"} \u2014 Multiple GitHub Project priority fields look plausible. Select the one Symphony should use:`,
2037
+ options: [
2038
+ ...priorityCandidates.map((field) => ({
2039
+ value: field.id,
2040
+ label: field.name,
2041
+ hint: `${field.options.length} option${field.options.length === 1 ? "" : "s"}`
2042
+ })),
2043
+ {
2044
+ value: "__skip_priority_field__",
2045
+ label: "Skip priority-aware dispatch",
2046
+ hint: "Leave tracker.priority_field unset"
2047
+ }
2048
+ ]
2049
+ })
2050
+ );
2051
+ if (selectedFieldId === "__skip_priority_field__") {
2052
+ return null;
2053
+ }
2054
+ return priorityCandidates.find((field) => field.id === selectedFieldId) ?? null;
2055
+ }
1504
2056
  async function promptStateMappings(statusField, options) {
1505
2057
  const mappings = {};
1506
2058
  const inferred = inferAllStateRoles(statusField.options.map((o) => o.name));
@@ -1529,12 +2081,15 @@ async function promptStateMappings(statusField, options) {
1529
2081
  return mappings;
1530
2082
  }
1531
2083
  async function planWorkflowArtifacts(opts) {
2084
+ const environment = opts.environment ?? await detectEnvironment(opts.cwd);
1532
2085
  const workflowMd = generateWorkflowMarkdown({
1533
2086
  projectId: opts.projectDetail.id,
1534
2087
  stateFieldName: opts.statusField.name,
2088
+ priorityFieldName: opts.priorityField?.name ?? null,
1535
2089
  mappings: opts.mappings,
1536
2090
  lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
1537
- runtime: opts.runtime
2091
+ runtime: opts.runtime,
2092
+ detectedEnvironment: environment
1538
2093
  });
1539
2094
  const workflowPlan = await planFileChange({
1540
2095
  path: opts.outputPath,
@@ -1546,9 +2101,11 @@ async function planWorkflowArtifacts(opts) {
1546
2101
  cwd: opts.cwd,
1547
2102
  projectDetail: opts.projectDetail,
1548
2103
  statusField: opts.statusField,
2104
+ priorityField: opts.priorityField,
1549
2105
  runtime: opts.runtime,
1550
2106
  skipSkills: opts.skipSkills,
1551
- skipContext: opts.skipContext
2107
+ skipContext: opts.skipContext,
2108
+ environment
1552
2109
  });
1553
2110
  return {
1554
2111
  outputPath: opts.outputPath,
@@ -1570,10 +2127,27 @@ function summarizeEnvironment(env) {
1570
2127
  ];
1571
2128
  }
1572
2129
  async function planEcosystem(opts) {
1573
- const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
2130
+ const {
2131
+ cwd,
2132
+ projectDetail,
2133
+ statusField,
2134
+ priorityField,
2135
+ runtime,
2136
+ skipSkills,
2137
+ skipContext
2138
+ } = opts;
1574
2139
  const ghSymphonyDir = join3(cwd, ".gh-symphony");
1575
- const environment = await detectEnvironment(cwd);
2140
+ const environment = opts.environment ?? await detectEnvironment(cwd);
1576
2141
  const files = [];
2142
+ files.push(
2143
+ await planFileChange({
2144
+ path: join3(cwd, DEFAULT_AFTER_CREATE_HOOK_PATH),
2145
+ label: DEFAULT_AFTER_CREATE_HOOK_LABEL,
2146
+ content: DEFAULT_AFTER_CREATE_HOOK_CONTENT,
2147
+ mode: "create-only",
2148
+ executable: true
2149
+ })
2150
+ );
1577
2151
  if (!skipContext) {
1578
2152
  const contextYaml = buildContextYaml({
1579
2153
  projectDetail,
@@ -1581,7 +2155,7 @@ async function planEcosystem(opts) {
1581
2155
  detectedEnvironment: environment,
1582
2156
  runtime: {
1583
2157
  agent: runtime,
1584
- agent_command: runtime === "codex" ? "bash -lc codex app-server" : runtime === "claude-code" ? "bash -lc claude-code" : runtime
2158
+ agent_command: resolveShellAgentCommand(runtime)
1585
2159
  }
1586
2160
  });
1587
2161
  files.push(
@@ -1599,7 +2173,9 @@ async function planEcosystem(opts) {
1599
2173
  name: o.name,
1600
2174
  role: null
1601
2175
  })),
1602
- projectId: projectDetail.id
2176
+ projectId: projectDetail.id,
2177
+ priorityFieldName: priorityField?.name ?? null,
2178
+ detectedEnvironment: environment
1603
2179
  });
1604
2180
  files.push(
1605
2181
  await planFileChange({
@@ -1630,7 +2206,8 @@ async function planEcosystem(opts) {
1630
2206
  })),
1631
2207
  statusFieldId: statusField.id,
1632
2208
  contextYamlPath: ".gh-symphony/context.yaml",
1633
- referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
2209
+ referenceWorkflowPath: ".gh-symphony/reference-workflow.md",
2210
+ detectedEnvironment: environment
1634
2211
  }
1635
2212
  );
1636
2213
  for (const plannedSkill of plannedSkills) {
@@ -1648,7 +2225,9 @@ async function planEcosystem(opts) {
1648
2225
  projectId: projectDetail.id,
1649
2226
  githubProjectTitle: projectDetail.title,
1650
2227
  runtime,
2228
+ priorityFieldName: priorityField?.name ?? null,
1651
2229
  skillsDir,
2230
+ skipSkills,
1652
2231
  environment,
1653
2232
  files
1654
2233
  };
@@ -1656,18 +2235,24 @@ async function planEcosystem(opts) {
1656
2235
  async function writeEcosystem(opts) {
1657
2236
  const plan = await planEcosystem(opts);
1658
2237
  await mkdir3(join3(opts.cwd, ".gh-symphony"), { recursive: true });
2238
+ const afterCreateHookPath = join3(opts.cwd, DEFAULT_AFTER_CREATE_HOOK_PATH);
1659
2239
  const contextYamlPath = join3(opts.cwd, ".gh-symphony", "context.yaml");
1660
2240
  const referenceWorkflowPath = join3(
1661
2241
  opts.cwd,
1662
2242
  ".gh-symphony",
1663
2243
  "reference-workflow.md"
1664
2244
  );
2245
+ let afterCreateHookWritten = false;
1665
2246
  let contextYamlWritten = false;
1666
2247
  let referenceWorkflowWritten = false;
1667
2248
  const skillsWritten = [];
1668
2249
  const skillsSkipped = [];
1669
2250
  for (const file of plan.files) {
1670
2251
  const written = await writePlannedFile(file);
2252
+ if (file.path === afterCreateHookPath) {
2253
+ afterCreateHookWritten = written;
2254
+ continue;
2255
+ }
1671
2256
  if (file.path === contextYamlPath) {
1672
2257
  contextYamlWritten = written;
1673
2258
  continue;
@@ -1689,7 +2274,10 @@ async function writeEcosystem(opts) {
1689
2274
  projectId: plan.projectId,
1690
2275
  githubProjectTitle: plan.githubProjectTitle,
1691
2276
  runtime: plan.runtime,
2277
+ priorityFieldName: plan.priorityFieldName,
1692
2278
  skillsDir: plan.skillsDir,
2279
+ skipSkills: plan.skipSkills,
2280
+ afterCreateHookWritten,
1693
2281
  contextYamlWritten,
1694
2282
  referenceWorkflowWritten,
1695
2283
  skillsWritten: skillsWritten.sort(),
@@ -1700,11 +2288,21 @@ function printEcosystemSummary(result, workflowPath, opts) {
1700
2288
  const cwd = process.cwd();
1701
2289
  const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
1702
2290
  const lines = [];
1703
- lines.push(`GitHub Project ${result.githubProjectTitle} (${result.projectId})`);
2291
+ lines.push(
2292
+ `GitHub Project ${result.githubProjectTitle} (${result.projectId})`
2293
+ );
1704
2294
  lines.push(`Runtime ${result.runtime}`);
2295
+ if (result.priorityFieldName) {
2296
+ lines.push(`Priority field ${result.priorityFieldName}`);
2297
+ }
1705
2298
  lines.push("");
1706
2299
  lines.push("Generated files");
1707
2300
  lines.push(` \u2713 WORKFLOW.md ${relWorkflow}`);
2301
+ if (result.afterCreateHookWritten) {
2302
+ lines.push(
2303
+ ` \u2713 ${DEFAULT_AFTER_CREATE_HOOK_LABEL.padEnd(36)} ${DEFAULT_AFTER_CREATE_HOOK_PATH}`
2304
+ );
2305
+ }
1708
2306
  if (result.contextYamlWritten) {
1709
2307
  lines.push(
1710
2308
  " \u2713 Context metadata .gh-symphony/context.yaml"
@@ -1725,7 +2323,7 @@ function printEcosystemSummary(result, workflowPath, opts) {
1725
2323
  for (const name of result.skillsSkipped) {
1726
2324
  lines.push(` \u2013 ${name} (already exists, skipped)`);
1727
2325
  }
1728
- } else if (result.runtime !== "codex" && result.runtime !== "claude-code") {
2326
+ } else if (!result.skipSkills) {
1729
2327
  lines.push("");
1730
2328
  lines.push("Skills \u2192 (skipped \u2014 custom runtime)");
1731
2329
  }
@@ -1750,6 +2348,9 @@ function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
1750
2348
  `GitHub Project ${ecosystemPlan.githubProjectTitle} (${ecosystemPlan.projectId})`
1751
2349
  );
1752
2350
  lines.push(`Runtime ${ecosystemPlan.runtime}`);
2351
+ if (ecosystemPlan.priorityFieldName) {
2352
+ lines.push(`Priority field ${ecosystemPlan.priorityFieldName}`);
2353
+ }
1753
2354
  lines.push("");
1754
2355
  lines.push("Planned file changes");
1755
2356
  lines.push(
@@ -1777,6 +2378,7 @@ function buildDryRunJsonResult(workflowPath, workflowPlan, ecosystemPlan) {
1777
2378
  projectId: ecosystemPlan.projectId,
1778
2379
  githubProjectTitle: ecosystemPlan.githubProjectTitle,
1779
2380
  runtime: ecosystemPlan.runtime,
2381
+ priorityFieldName: ecosystemPlan.priorityFieldName,
1780
2382
  files: [workflowPlan, ...ecosystemPlan.files].map((file) => ({
1781
2383
  path: file.path,
1782
2384
  label: file.label,
@@ -1792,6 +2394,18 @@ function printDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
1792
2394
  );
1793
2395
  }
1794
2396
  async function runNonInteractive(flags, options) {
2397
+ const runtime = resolveInitRuntime(flags.runtime);
2398
+ const runtimeError = validateInitRuntime(runtime);
2399
+ if (runtimeError) {
2400
+ process.stderr.write(`Error: ${runtimeError}
2401
+ `);
2402
+ process.exitCode = 1;
2403
+ return;
2404
+ }
2405
+ if (!await runInitRuntimePreflight(runtime)) {
2406
+ process.exitCode = 1;
2407
+ return;
2408
+ }
1795
2409
  let token;
1796
2410
  try {
1797
2411
  token = getGhTokenWithSource().token;
@@ -1849,6 +2463,13 @@ async function runNonInteractive(flags, options) {
1849
2463
  return;
1850
2464
  }
1851
2465
  const mappings = buildAutomaticStateMappings(statusField);
2466
+ const { field: autoPriorityField, ambiguous: ambiguousPriorityFields } = resolvePriorityField(githubProject, statusField);
2467
+ if (ambiguousPriorityFields.length > 0) {
2468
+ process.stderr.write(
2469
+ `Warning: Multiple priority-like single-select fields found (${ambiguousPriorityFields.map((field) => `"${field.name}"`).join(", ")}). Skipping tracker.priority_field in non-interactive mode.
2470
+ `
2471
+ );
2472
+ }
1852
2473
  const validation = validateStateMapping(mappings);
1853
2474
  if (!validation.valid) {
1854
2475
  process.stderr.write(
@@ -1865,8 +2486,9 @@ Run without --non-interactive for manual mapping.
1865
2486
  outputPath,
1866
2487
  projectDetail: githubProject,
1867
2488
  statusField,
2489
+ priorityField: autoPriorityField,
1868
2490
  mappings,
1869
- runtime: "codex",
2491
+ runtime,
1870
2492
  skipSkills: flags.skipSkills,
1871
2493
  skipContext: flags.skipContext
1872
2494
  });
@@ -1887,7 +2509,8 @@ Run without --non-interactive for manual mapping.
1887
2509
  cwd: process.cwd(),
1888
2510
  projectDetail: githubProject,
1889
2511
  statusField,
1890
- runtime: "codex",
2512
+ priorityField: autoPriorityField,
2513
+ runtime,
1891
2514
  skipSkills: flags.skipSkills,
1892
2515
  skipContext: flags.skipContext
1893
2516
  });
@@ -1907,6 +2530,15 @@ async function runInteractive(flags, options) {
1907
2530
  await runInteractiveStandalone(flags, options);
1908
2531
  }
1909
2532
  async function runInteractiveStandalone(flags, _options) {
2533
+ const runtime = await promptRuntimeSelection();
2534
+ if (isClaudeRuntime(runtime)) {
2535
+ if (!await runInitRuntimePreflight(runtime)) {
2536
+ process.exitCode = 1;
2537
+ return;
2538
+ }
2539
+ } else {
2540
+ runRuntimePreflight(runtime);
2541
+ }
1910
2542
  const s1 = p.spinner();
1911
2543
  s1.start("Checking GitHub authentication...");
1912
2544
  let client;
@@ -1929,10 +2561,12 @@ async function runInteractiveStandalone(flags, _options) {
1929
2561
  s2.start("Loading projects...");
1930
2562
  let projects;
1931
2563
  try {
1932
- projects = await listUserProjects(client);
2564
+ const discovery = await discoverUserProjects(client);
2565
+ projects = discovery.projects;
1933
2566
  s2.stop(
1934
2567
  `Found ${projects.length} project${projects.length === 1 ? "" : "s"}`
1935
2568
  );
2569
+ warnIfProjectDiscoveryPartial(discovery);
1936
2570
  } catch (error) {
1937
2571
  s2.stop("Failed to load projects.");
1938
2572
  if (error instanceof GitHubScopeError) {
@@ -1952,7 +2586,7 @@ async function runInteractiveStandalone(flags, _options) {
1952
2586
  }
1953
2587
  const selectedGithubProjectId = await abortIfCancelled(
1954
2588
  p.select({
1955
- message: "Step 1/2 \u2014 Select a GitHub Project board:",
2589
+ message: "Step 2/3 \u2014 Select a GitHub Project board:",
1956
2590
  options: projects.map((proj) => ({
1957
2591
  value: proj.id,
1958
2592
  label: `${proj.owner.login}/${proj.title}`,
@@ -1981,7 +2615,16 @@ async function runInteractiveStandalone(flags, _options) {
1981
2615
  process.exitCode = 1;
1982
2616
  return;
1983
2617
  }
1984
- const mappings = await promptStateMappings(statusField);
2618
+ const priorityResolution = resolvePriorityField(projectDetail, statusField);
2619
+ const mappings = await promptStateMappings(statusField, {
2620
+ stepLabel: priorityResolution.ambiguous.length > 0 ? "Step 3/4" : "Step 3/3"
2621
+ });
2622
+ let priorityField = priorityResolution.field;
2623
+ if (priorityResolution.ambiguous.length > 0) {
2624
+ priorityField = await promptPriorityField(priorityResolution.ambiguous, {
2625
+ stepLabel: "Step 4/4"
2626
+ });
2627
+ }
1985
2628
  const validation = validateStateMapping(mappings);
1986
2629
  if (!validation.valid) {
1987
2630
  p.log.error("Mapping validation failed:");
@@ -2000,8 +2643,9 @@ async function runInteractiveStandalone(flags, _options) {
2000
2643
  outputPath,
2001
2644
  projectDetail,
2002
2645
  statusField,
2646
+ priorityField,
2003
2647
  mappings,
2004
- runtime: "codex",
2648
+ runtime,
2005
2649
  skipSkills: flags.skipSkills,
2006
2650
  skipContext: flags.skipContext
2007
2651
  });
@@ -2014,7 +2658,8 @@ async function runInteractiveStandalone(flags, _options) {
2014
2658
  cwd: process.cwd(),
2015
2659
  projectDetail,
2016
2660
  statusField,
2017
- runtime: "codex",
2661
+ priorityField,
2662
+ runtime,
2018
2663
  skipSkills: flags.skipSkills,
2019
2664
  skipContext: flags.skipContext
2020
2665
  });
@@ -2061,10 +2706,12 @@ function generateProjectId(githubProjectTitle, uniqueKey) {
2061
2706
 
2062
2707
  export {
2063
2708
  validateStateMapping,
2709
+ warnIfProjectDiscoveryPartial,
2064
2710
  abortIfCancelled,
2065
2711
  init_default,
2066
2712
  resolveStatusField,
2067
2713
  buildAutomaticStateMappings,
2714
+ resolvePriorityField,
2068
2715
  promptStateMappings,
2069
2716
  planWorkflowArtifacts,
2070
2717
  writeWorkflowPlan,