From fc43c5bd3dbf152c59fc155ad252b9edf8ea5cbd Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 5 Sep 2026 19:23:07 +0900 Subject: [PATCH 1/2] fix(cli): report effective workflow timeouts --- README.md | 2 +- docs/configuration.md | 5 ++ packages/cli/README.md | 5 ++ packages/cli/src/commands/workflow.test.ts | 67 ++++++++++++++++++++++ packages/cli/src/commands/workflow.ts | 13 +++-- 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8ce2276c..385c6ab3 100644 --- a/README.md +++ b/README.md @@ -801,7 +801,7 @@ gh-symphony workflow init --non-interactive --project PVT_xxx --output WORKFLOW. gh-symphony workflow init --non-interactive --project PVT_xxx --dry-run ``` -`gh-symphony workflow validate` parses the target file, strictly renders the prompt body and continuation guidance with canonical sample variables, and prints a compact runtime/lifecycle summary. +`gh-symphony workflow validate` parses the target file, strictly renders the prompt body and continuation guidance with canonical sample variables, and prints a compact runtime/lifecycle summary. Its `runtime.timeouts.*` values are the effective runtime settings: `runtime.timeouts` takes precedence over the legacy `codex.*_timeout_ms` fields, with documented defaults used when neither is configured. `gh-symphony workflow preview --issue owner/repo#123` is the fastest validation step after `workflow init`: it resolves the active managed project (or `--project-id`) and renders the exact worker prompt from the live GitHub Project issue. Linear workflows can preview a single issue with `gh-symphony workflow preview ENG-123`, which routes through the configured Linear tracker adapter and `LINEAR_API_KEY`. Keep `--sample ` for fixture-based debugging, and use `--attempt ` to inspect retry prompts before changing policy files. diff --git a/docs/configuration.md b/docs/configuration.md index 5b9c0db0..d3052855 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,6 +83,11 @@ negative, that threshold is disabled, but the orchestrator still applies a hard 30-minute elapsed-run fallback to prevent an indefinitely stuck worker. This is an intentional implementation-defined safety limit. +`runtime.timeouts` is the preferred timeout configuration and takes precedence +over the legacy `codex.*_timeout_ms` fields. `gh-symphony workflow validate` +prints the effective values under `runtime.timeouts.*`, matching the values the +orchestrator injects into a worker. + Hooks are opt-in repository-local extensions, not shell snippets. Each hook value must be a path to an executable script; shell syntax and inline commands are rejected. Set `SYMPHONY_ALLOW_WORKFLOW_HOOKS=1` (or `true`) in the host diff --git a/packages/cli/README.md b/packages/cli/README.md index 03f0f96f..66bed271 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -167,6 +167,11 @@ unanswerable request. To migrate an existing workflow, change either value to maximum silence interval for a Codex app-server turn. Every app-server output resets it; it is not a total turn-duration cap. +`gh-symphony workflow validate` reports the effective values under +`runtime.timeouts.*`. An explicit `runtime.timeouts` block takes precedence over +the legacy `codex.*_timeout_ms` fields; documented defaults apply when neither +location provides a value. + Lifecycle generation enables blocker checks for the first configured active state (`Todo` with built-in defaults) while leaving planning states disabled. An explicit `tracker.provider.blocker_check_states: []` disables blocker gating; this is diff --git a/packages/cli/src/commands/workflow.test.ts b/packages/cli/src/commands/workflow.test.ts index fee2370d..96867a64 100644 --- a/packages/cli/src/commands/workflow.test.ts +++ b/packages/cli/src/commands/workflow.test.ts @@ -122,6 +122,73 @@ describe("workflow command handler", () => { expect(stdout.output()).toContain("active_states=Ready, In progress"); }); + it.each([ + { + name: "runtime timeout precedence", + frontMatter: `runtime: + kind: codex-app-server + command: codex + args: [app-server] + timeouts: + read_timeout_ms: 30000 + stall_timeout_ms: 900000 + turn_timeout_ms: 1800000 +codex: + read_timeout_ms: 7000 + stall_timeout_ms: 60000 + turn_timeout_ms: 120000`, + expected: [30000, 900000, 1800000], + }, + { + name: "legacy codex timeout fallback", + frontMatter: `codex: + read_timeout_ms: 7000 + stall_timeout_ms: 60000 + turn_timeout_ms: 120000`, + expected: [7000, 60000, 120000], + }, + { + name: "documented timeout defaults", + frontMatter: "codex:\n command: codex app-server", + expected: [5000, 300000, 3600000], + }, + ])( + "reports $name as effective runtime timeouts", + async ({ frontMatter, expected }) => { + const root = await mkdtemp(join(tmpdir(), "workflow-validate-timeouts-")); + const workflowPath = join(root, "WORKFLOW.md"); + const stdout = captureWrites(process.stdout); + + await writeFile( + workflowPath, + `---\ntracker:\n kind: github-project\n${frontMatter}\n---\nPrompt {{ issue.identifier }}\n`, + "utf8" + ); + + try { + await workflowCommand(["validate", "--file", workflowPath], { + configDir: root, + verbose: false, + json: false, + noColor: false, + }); + } finally { + stdout.restore(); + } + + expect(stdout.output()).toContain( + `runtime.timeouts.read_timeout_ms=${expected[0]}` + ); + expect(stdout.output()).toContain( + `runtime.timeouts.stall_timeout_ms=${expected[1]}` + ); + expect(stdout.output()).toContain( + `runtime.timeouts.turn_timeout_ms=${expected[2]}` + ); + expect(stdout.output()).not.toContain("codex.read_timeout_ms="); + } + ); + it("prints a typed error when a removed flat tracker key is configured", async () => { const root = await mkdtemp(join(tmpdir(), "workflow-validate-priority-")); const workflowPath = join(root, "WORKFLOW.md"); diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 401cc6ea..218292c4 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -7,6 +7,7 @@ import { WorkflowValidationError, renderPrompt, resolveWorkflowExecutionPhase, + resolveWorkflowRuntimeTimeouts, type TrackedIssue, } from "@gh-symphony/core"; import { @@ -105,6 +106,8 @@ type WorkflowValidationReport = { approvalPolicy: string | null; threadSandbox: string | null; turnSandboxPolicy: string | null; + }; + runtimeTimeouts: { readTimeoutMs: number; stallTimeoutMs: number; turnTimeoutMs: number; @@ -878,10 +881,8 @@ function validateWorkflow( approvalPolicy: workflow.codex.approvalPolicy, threadSandbox: workflow.codex.threadSandbox, turnSandboxPolicy: workflow.codex.turnSandboxPolicy, - readTimeoutMs: workflow.codex.readTimeoutMs, - stallTimeoutMs: workflow.codex.stallTimeoutMs, - turnTimeoutMs: workflow.codex.turnTimeoutMs, }, + runtimeTimeouts: resolveWorkflowRuntimeTimeouts(workflow), hooks: { afterCreate: workflow.hooks.afterCreate, beforeRun: workflow.hooks.beforeRun, @@ -920,9 +921,9 @@ Runtime codex.approval_policy=${report.summary.codex.approvalPolicy ?? "unset"} codex.thread_sandbox=${report.summary.codex.threadSandbox ?? "unset"} codex.turn_sandbox_policy=${report.summary.codex.turnSandboxPolicy ?? "unset"} - codex.read_timeout_ms=${report.summary.codex.readTimeoutMs} - codex.stall_timeout_ms=${report.summary.codex.stallTimeoutMs} - codex.turn_timeout_ms=${report.summary.codex.turnTimeoutMs} + runtime.timeouts.read_timeout_ms=${report.summary.runtimeTimeouts.readTimeoutMs} + runtime.timeouts.stall_timeout_ms=${report.summary.runtimeTimeouts.stallTimeoutMs} + runtime.timeouts.turn_timeout_ms=${report.summary.runtimeTimeouts.turnTimeoutMs} Hooks after_create=${report.summary.hooks.afterCreate ?? "unset"} From 2a51e85d844646f7b1ae78fcd673c17085333c59 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sat, 5 Sep 2026 20:24:44 +0900 Subject: [PATCH 2/2] fix(cli): stabilize timeout validation report --- .changeset/tidy-pandas-report.md | 5 +++ packages/cli/README.md | 5 +++ packages/cli/src/commands/workflow.test.ts | 44 +++++++++++++++++++++- packages/cli/src/commands/workflow.ts | 17 +++++++-- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 .changeset/tidy-pandas-report.md diff --git a/.changeset/tidy-pandas-report.md b/.changeset/tidy-pandas-report.md new file mode 100644 index 00000000..271a4ad9 --- /dev/null +++ b/.changeset/tidy-pandas-report.md @@ -0,0 +1,5 @@ +--- +"@gh-symphony/cli": patch +--- + +Report the effective runtime timeout values and their configuration source from `workflow validate`, including a stable three-field JSON timeout object (#882). diff --git a/packages/cli/README.md b/packages/cli/README.md index 66bed271..25e71a8b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -172,6 +172,11 @@ resets it; it is not a total turn-duration cap. the legacy `codex.*_timeout_ms` fields; documented defaults apply when neither location provides a value. +In JSON output, effective timeout values are exposed as +`summary.runtimeTimeouts.{readTimeoutMs,stallTimeoutMs,turnTimeoutMs}`. These +replace the former `summary.codex.*TimeoutMs` fields, which could report values +that the runtime did not use; no compatibility aliases are emitted. + Lifecycle generation enables blocker checks for the first configured active state (`Todo` with built-in defaults) while leaving planning states disabled. An explicit `tracker.provider.blocker_check_states: []` disables blocker gating; this is diff --git a/packages/cli/src/commands/workflow.test.ts b/packages/cli/src/commands/workflow.test.ts index 96867a64..9c9a437f 100644 --- a/packages/cli/src/commands/workflow.test.ts +++ b/packages/cli/src/commands/workflow.test.ts @@ -138,6 +138,7 @@ codex: stall_timeout_ms: 60000 turn_timeout_ms: 120000`, expected: [30000, 900000, 1800000], + expectedSource: "runtime.timeouts", }, { name: "legacy codex timeout fallback", @@ -146,15 +147,30 @@ codex: stall_timeout_ms: 60000 turn_timeout_ms: 120000`, expected: [7000, 60000, 120000], + expectedSource: "codex/defaults", + }, + { + name: "runtime defaults over legacy codex timeouts", + frontMatter: `runtime: + kind: codex-app-server + command: codex + args: [app-server] +codex: + read_timeout_ms: 7000 + stall_timeout_ms: 60000 + turn_timeout_ms: 120000`, + expected: [5000, 300000, 3600000], + expectedSource: "runtime.timeouts", }, { name: "documented timeout defaults", frontMatter: "codex:\n command: codex app-server", expected: [5000, 300000, 3600000], + expectedSource: "codex/defaults", }, ])( "reports $name as effective runtime timeouts", - async ({ frontMatter, expected }) => { + async ({ frontMatter, expected, expectedSource }) => { const root = await mkdtemp(join(tmpdir(), "workflow-validate-timeouts-")); const workflowPath = join(root, "WORKFLOW.md"); const stdout = captureWrites(process.stdout); @@ -186,6 +202,32 @@ codex: `runtime.timeouts.turn_timeout_ms=${expected[2]}` ); expect(stdout.output()).not.toContain("codex.read_timeout_ms="); + expect(stdout.output()).toContain(`(source: ${expectedSource})`); + + const jsonStdout = captureWrites(process.stdout); + try { + await workflowCommand(["validate", "--file", workflowPath], { + configDir: root, + verbose: false, + json: true, + noColor: false, + }); + } finally { + jsonStdout.restore(); + } + + const report = JSON.parse(jsonStdout.output()) as { + summary: { + runtimeTimeouts: Record; + runtimeTimeoutSource: string; + }; + }; + expect(report.summary.runtimeTimeouts).toEqual({ + readTimeoutMs: expected[0], + stallTimeoutMs: expected[1], + turnTimeoutMs: expected[2], + }); + expect(report.summary.runtimeTimeoutSource).toBe(expectedSource); } ); diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 218292c4..fa8db6a3 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -112,6 +112,7 @@ type WorkflowValidationReport = { stallTimeoutMs: number; turnTimeoutMs: number; }; + runtimeTimeoutSource: "runtime.timeouts" | "codex/defaults"; hooks: { afterCreate: string | null; beforeRun: string | null; @@ -846,6 +847,7 @@ function validateWorkflow( return "pass" as const; })() : ("skip" as const); + const effectiveTimeouts = resolveWorkflowRuntimeTimeouts(workflow); return { ok: true, @@ -882,7 +884,14 @@ function validateWorkflow( threadSandbox: workflow.codex.threadSandbox, turnSandboxPolicy: workflow.codex.turnSandboxPolicy, }, - runtimeTimeouts: resolveWorkflowRuntimeTimeouts(workflow), + runtimeTimeouts: { + readTimeoutMs: effectiveTimeouts.readTimeoutMs, + stallTimeoutMs: effectiveTimeouts.stallTimeoutMs, + turnTimeoutMs: effectiveTimeouts.turnTimeoutMs, + }, + runtimeTimeoutSource: workflow.runtime + ? "runtime.timeouts" + : "codex/defaults", hooks: { afterCreate: workflow.hooks.afterCreate, beforeRun: workflow.hooks.beforeRun, @@ -921,9 +930,9 @@ Runtime codex.approval_policy=${report.summary.codex.approvalPolicy ?? "unset"} codex.thread_sandbox=${report.summary.codex.threadSandbox ?? "unset"} codex.turn_sandbox_policy=${report.summary.codex.turnSandboxPolicy ?? "unset"} - runtime.timeouts.read_timeout_ms=${report.summary.runtimeTimeouts.readTimeoutMs} - runtime.timeouts.stall_timeout_ms=${report.summary.runtimeTimeouts.stallTimeoutMs} - runtime.timeouts.turn_timeout_ms=${report.summary.runtimeTimeouts.turnTimeoutMs} + runtime.timeouts.read_timeout_ms=${report.summary.runtimeTimeouts.readTimeoutMs} (source: ${report.summary.runtimeTimeoutSource}) + runtime.timeouts.stall_timeout_ms=${report.summary.runtimeTimeouts.stallTimeoutMs} (source: ${report.summary.runtimeTimeoutSource}) + runtime.timeouts.turn_timeout_ms=${report.summary.runtimeTimeouts.turnTimeoutMs} (source: ${report.summary.runtimeTimeoutSource}) Hooks after_create=${report.summary.hooks.afterCreate ?? "unset"}