From d5f6dac6adcbac21567c0303a3dcdcbb53aab79d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 21:52:28 +0000 Subject: [PATCH 1/3] Headless Claude dispatch: print argv, systemd unit, stream-json projection. When the harness has no spark, ExecutorDefault can admit Claude if custody is present; events stay in the belt's Codex envelope. Credential converge on srv1 remains an operator decision. Co-authored-by: Cursor --- dag/extdeps/llm/claude_code_stream_json.dag | 60 +++++- dag/extdeps/llm/cli.dag | 50 +++++ dag/gunbc/claude_code_limit_standing.dag | 9 + .../roadmap/roadmap_dispatch_actuator.dag | 175 ++++++++++++------ dag/gunbc/roadmap/roadmap_provider_events.dag | 45 ++++- .../claude_code_dispatch_witness_test.dag | 15 ++ .../extdeps_llm_claude_trust_witness_test.dag | 30 +++ ...roadmap_dispatch_actuator_witness_test.dag | 67 +++++-- .../roadmap_provider_events_witness_test.dag | 61 +++++- 9 files changed, 433 insertions(+), 79 deletions(-) diff --git a/dag/extdeps/llm/claude_code_stream_json.dag b/dag/extdeps/llm/claude_code_stream_json.dag index 489451c25c8..a05928e1a00 100644 --- a/dag/extdeps/llm/claude_code_stream_json.dag +++ b/dag/extdeps/llm/claude_code_stream_json.dag @@ -76,6 +76,11 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope { } data claude_code_stream_event_type_result: String = "result" +data claude_code_stream_event_type_system: String = "system" +data claude_code_stream_event_type_assistant: String = "assistant" +data claude_code_stream_event_type_user: String = "user" +data claude_code_content_block_type_tool_use: String = "tool_use" +data claude_code_content_block_type_tool_result: String = "tool_result" data claude_code_rate_limit_status_rejected: String = "rejected" // The argv words that make the CLI write this wire. --verbose is required by the CLI for @@ -112,6 +117,9 @@ type ClaudeCodeStreamLineGap type ClaudeCodeStreamLine = ClaudeCodeRateLimitEventLine { info: ClaudeCodeRateLimitInfo } | ClaudeCodeResultLine { result: ClaudeCodeTurnResult } + | ClaudeCodeSystemLine + | ClaudeCodeAssistantLine { has_tool_use: Bool } + | ClaudeCodeUserLine { has_tool_result: Bool } | ClaudeCodeOtherLine { event_type: String } | ClaudeCodeLineUnreadable { gap: ClaudeCodeStreamLineGap } @@ -425,6 +433,40 @@ fn claude_result_line(doc: JsonValue) -> ClaudeCodeStreamLine { } } +fn claude_content_blocks_have_type(content: JsonValue, block_type: String) -> Bool { + match content { + JsonArray { elements } => + any(elements, e => + match claude_string_member(v: e, key: "type") { + StringMemberRead { value: s } => s == block_type + StringMemberNull => false + StringMemberAbsent => false + StringMemberMalformed => false + } + ) + JsonNull => false + JsonBool { value: _ } => false + JsonNumber { lexeme: _ } => false + JsonString { value: _ } => false + JsonObject { members: _ } => false + } +} + +fn claude_message_content_has_type(doc: JsonValue, block_type: String) -> Bool { + match json_object_unique_member(v: doc, key: "message") { + JsonMemberFound { value: msg } => + match json_object_unique_member(v: msg, key: "content") { + JsonMemberFound { value: content } => claude_content_blocks_have_type(content: content, block_type: block_type) + JsonMemberAbsent => false + JsonMemberDuplicated { count: _ } => false + JsonMemberNotAnObject => false + } + JsonMemberAbsent => false + JsonMemberDuplicated { count: _ } => false + JsonMemberNotAnObject => false + } +} + fn claude_code_stream_line(line: String) -> ClaudeCodeStreamLine { match parse_json_document(s: line) { JsonDocumentUnreadable { gap: _ } => ClaudeCodeLineUnreadable { gap: StreamLineNotJson } @@ -442,7 +484,23 @@ fn claude_code_stream_line(line: String) -> ClaudeCodeStreamLine { if t == claude_code_stream_event_type_result { claude_result_line(doc: doc) } else { - ClaudeCodeOtherLine { event_type: t } + if t == claude_code_stream_event_type_system { + ClaudeCodeSystemLine + } else { + if t == claude_code_stream_event_type_assistant { + ClaudeCodeAssistantLine { + has_tool_use: claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_use), + } + } else { + if t == claude_code_stream_event_type_user { + ClaudeCodeUserLine { + has_tool_result: claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_result), + } + } else { + ClaudeCodeOtherLine { event_type: t } + } + } + } } } StringMemberNull => ClaudeCodeLineUnreadable { gap: StreamLineMemberMissing { member: "type" } } diff --git a/dag/extdeps/llm/cli.dag b/dag/extdeps/llm/cli.dag index 0ee78d5949e..ce2923d3f04 100644 --- a/dag/extdeps/llm/cli.dag +++ b/dag/extdeps/llm/cli.dag @@ -5,6 +5,7 @@ import extdeps.tools.env { env_path_resolved_program } import std.algebra { trim } import std.types { List, Int, Bool, NonEmptyStr, RenderedTerminalText, Unit } +import extdeps.llm.claude_code_stream_json { claude_code_stream_json_print_args } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.languages.json.grammar { @@ -201,6 +202,55 @@ fn claude_model_args(selection: ProviderModelSelection) -> List { } } +// HEADLESS PRINT ARGV. Interactive `claude` (shape_claude_invoke_argv) is a session in a terminal: +// it takes --session-id, --settings, --effort and a node name. Automation is a different surface of +// the same binary: `claude -p --output-format stream-json --verbose` writes one JSON object per +// stdout line (extdeps.llm.claude_code_stream_json). The Agent SDK's query() spawns that same +// process; this row is that execution without a Node runtime. --verbose is required by the CLI for +// stream-json under -p. Turn bound is a coproduct so "no --max-turns" cannot be confused with a +// sentinel count. Permission flags and the start prompt stay caller-supplied: skip-permissions is +// gunbc policy (dispatch_claude_permission_args), not an upstream default. + +type ClaudePrintTurnBound + = ClaudePrintUnboundedTurns + | ClaudePrintMaxTurns { turns: Int } + +data claude_cli_max_turns_flag: String = "--max-turns" + +data claude_cli_append_system_prompt_flag: String = "--append-system-prompt" + +fn claude_print_max_turns_args(bound: ClaudePrintTurnBound) -> List { + match bound { + ClaudePrintUnboundedTurns => [] + ClaudePrintMaxTurns { turns } => [claude_cli_max_turns_flag, to_string(turns)] + } +} + +fn shape_claude_print_argv( + append_system_prompt: String, + start_prompt: String, + permission_args: List, + model: ProviderModelSelection, + turn_bound: ClaudePrintTurnBound, +) -> List { + concat( + [claude_cli_program], + concat( + claude_code_stream_json_print_args, + concat( + claude_print_max_turns_args(bound: turn_bound), + concat( + [claude_cli_append_system_prompt_flag, append_system_prompt], + concat( + claude_model_args(selection: model), + concat(permission_args, [start_prompt]), + ), + ), + ), + ), + ) +} + fn codex_model_args(selection: ProviderModelSelection) -> List { match selection { ProviderAccountDefaultModel => [] diff --git a/dag/gunbc/claude_code_limit_standing.dag b/dag/gunbc/claude_code_limit_standing.dag index dbbb4a553ab..2e08a4e1e88 100644 --- a/dag/gunbc/claude_code_limit_standing.dag +++ b/dag/gunbc/claude_code_limit_standing.dag @@ -9,6 +9,9 @@ import extdeps.llm.claude_code_stream_json { ClaudeCodeRateLimitEventLine, ClaudeCodeResultLine, ClaudeCodeOtherLine, + ClaudeCodeSystemLine, + ClaudeCodeAssistantLine, + ClaudeCodeUserLine, ClaudeCodeLineUnreadable, ClaudeCodeRateLimitInfo, ClaudeCodeRateLimitWindow, @@ -105,6 +108,12 @@ fn claude_code_trip_reading(stdout: String) -> ClaudeCodeTripReading { ClaudeTripFold { index: acc.index + 1, reading: ClaudeCodeTripRead { result: Present { value: result }, rate_limit: l } } ClaudeCodeOtherLine { event_type: _ } => ClaudeTripFold { index: acc.index + 1, reading: acc.reading } + ClaudeCodeSystemLine => + ClaudeTripFold { index: acc.index + 1, reading: acc.reading } + ClaudeCodeAssistantLine { has_tool_use: _ } => + ClaudeTripFold { index: acc.index + 1, reading: acc.reading } + ClaudeCodeUserLine { has_tool_result: _ } => + ClaudeTripFold { index: acc.index + 1, reading: acc.reading } ClaudeCodeLineUnreadable { gap } => ClaudeTripFold { index: acc.index + 1, reading: ClaudeCodeTripUnreadable { line_index: acc.index, gap: gap } } } diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index 73023923c91..85f2d7ba93c 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -87,7 +87,8 @@ import extdeps.tmux { import extdeps.llm.cli { ProviderModelSelection, ProviderModelPinned, ProviderAccountDefaultModel, - shape_claude_invoke_argv, + shape_claude_print_argv, + ClaudePrintUnboundedTurns, claude_cli_program, codex_cli_program, shape_codex_exec_argv, @@ -464,10 +465,15 @@ fn dispatch_actuator_selection_for_provider_on( // per-window usage limits) does not admit the draw, refuses typed, and is never replaced by the // harness. Absence of a request is ExecutorDefault, never a vendor. // -// DECLARED FRONTIER, NOT CODE: automatic selection between the harness and a vendor is a std.decision -// select_realization over funded axes (cost, usage-window headroom, latency, quality). Its trigger is -// a grounded funded-axis policy once limit, latency and quality readings exist for at least two -// providers; until then nothing here chooses on the operator's behalf. +// AUTOMATIC RANKING BETWEEN HARNESS AND VENDOR REMAINS A FRONTIER (std.decision select_realization +// over funded axes: cost, usage-window headroom, latency, quality). That trigger is unchanged: a +// grounded funded-axis policy once those readings exist for at least two providers. +// +// WHAT THIS FUNCTION DOES LIFT is not that ranking. ExecutorDefault still prefers a serving harness +// route. When the harness has no enrolled serving route AND the Claude custody file is present on +// this host, the default admits the same observed-Claude path an explicit `?executor=claude` already +// takes. Absence of custody keeps the harness refusal. That is fail-closed admission of the only +// remaining executing backend, not a Pareto choice among two live ones. type DispatchExecutorRequest = ExecutorDefault | ExecutorVendor { provider: DispatchCliProvider } @@ -677,7 +683,25 @@ fn dispatch_actuator_selection_for_request( sizing_expectation: SizingProfileExpectation, ) -> DispatchActuatorSelection { match request { - ExecutorDefault => dispatch_actuator_selection(sizing_expectation: sizing_expectation) + ExecutorDefault => + match dispatch_harness_selection() { + DispatchActuatorSelectionOk { provider, effort, model, process_fingerprint } => + DispatchActuatorSelectionOk { + provider: provider, + effort: effort, + model: model, + process_fingerprint: process_fingerprint, + } + DispatchActuatorSelectionRefused { reason: harness_reason } => + match claude_credential_custody_source() { + CredentialSnapshotTaken { path: _ } => + dispatch_actuator_claude_selection_observed( + instance: instance, node_id: node_id, attempt_key: attempt_key, sizing_expectation: sizing_expectation, + ) + CredentialSnapshotRefused { reason: _ } => + DispatchActuatorSelectionRefused { reason: harness_reason } + } + } ExecutorVendor { provider } => match provider { ClaudeCodeProvider => @@ -2194,21 +2218,15 @@ fn git_worktree_add_detached_argv_for_instance( } fn claude_spawn_argv( - effort: ReasoningEffort, model: ProviderModelSelection, - session_uuid: String, - node_id: RoadmapNodeId, brief: String, ) -> List { - shape_claude_invoke_argv( - session_id: session_uuid, - node_id: node_id, - effort: effort, - model: model, - settings_json: claude_skip_permission_settings_json, + shape_claude_print_argv( append_system_prompt: brief, start_prompt: dispatch_claude_start_prompt, permission_args: dispatch_claude_permission_args, + model: model, + turn_bound: ClaudePrintUnboundedTurns, ) } @@ -2333,10 +2351,7 @@ fn dispatch_provider_inner_argv( argv: concat( [env_path_resolved_program().invocation as String, "CLAUDE_CODE_NO_FLICKER=1"], claude_spawn_argv( - effort: effort, model: model, - session_uuid: session_uuid, - node_id: node_id, brief: brief, ), ), @@ -2416,10 +2431,7 @@ fn dispatch_provider_inner_argv_for_instance( "CLAUDE_CODE_NO_FLICKER=1", ], claude_spawn_argv( - effort: effort, model: model, - session_uuid: session_uuid, - node_id: node_id, brief: brief, ), ), @@ -2659,12 +2671,30 @@ fn dispatch_worker_unit_properties( worker_log_path: String, granted: Kibibyte, process_bounds: UnitProcessBounds, +) -> List { + dispatch_worker_unit_stream_properties( + instance: instance, + working_directory: working_directory, + stdout_path: worker_log_path, + stderr_path: worker_log_path, + granted: granted, + process_bounds: process_bounds, + ) +} + +fn dispatch_worker_unit_stream_properties( + instance: HostDashboardInstance, + working_directory: String, + stdout_path: String, + stderr_path: String, + granted: Kibibyte, + process_bounds: UnitProcessBounds, ) -> List { concat( compute_grant_unit_properties(granted: granted, working_directory: working_directory, process_bounds: Present { value: process_bounds }), [ - SystemdRunProperty { property: StandardOutputProperty, value: join(["append:", worker_log_path], "") as NonEmptyStr }, - SystemdRunProperty { property: StandardErrorProperty, value: join(["append:", worker_log_path], "") as NonEmptyStr }, + SystemdRunProperty { property: StandardOutputProperty, value: join(["append:", stdout_path], "") as NonEmptyStr }, + SystemdRunProperty { property: StandardErrorProperty, value: join(["append:", stderr_path], "") as NonEmptyStr }, SystemdRunProperty { property: ProtectSystemProperty, value: "strict" as NonEmptyStr }, SystemdRunProperty { property: ProtectHomeProperty, value: "read-only" as NonEmptyStr }, SystemdRunProperty { property: PrivateTmpProperty, value: "yes" as NonEmptyStr }, @@ -2706,15 +2736,17 @@ type DispatchWorkerUnitPlan { systemd_run_program: FilePath unit: NonEmptyStr working_directory: String - worker_log_path: String + stdout_path: String + stderr_path: String inner_argv: List } fn dispatch_worker_unit_properties_for_plan(instance: HostDashboardInstance, plan: DispatchWorkerUnitPlan, granted: Kibibyte, process_bounds: UnitProcessBounds) -> List { - dispatch_worker_unit_properties( + dispatch_worker_unit_stream_properties( instance: instance, working_directory: plan.working_directory, - worker_log_path: plan.worker_log_path, + stdout_path: plan.stdout_path, + stderr_path: plan.stderr_path, granted: granted, process_bounds: process_bounds, ) @@ -2742,6 +2774,43 @@ type DispatchSessionContainerPlan // not be a degraded success, it would be a guaranteed HostBudgetUnreadable wearing the shape of a // spawn that worked. macbook_local_toolchain answers `none` here because macOS has no systemd at // all, which is why the field is optional rather than a path every toolchain must invent. +fn dispatch_systemd_unit_container_plan( + instance: HostDashboardInstance, + node_id: String, + attempt_key: String, + pane_working_directory: String, + stdout_path: String, + stderr_path: String, + provider_inner: List, + unavailable_step: NonEmptyStr, + unavailable_why: String, +) -> DispatchSessionContainerPlan { + match instance.toolchain.systemd_run { + Absent => + DispatchSessionContainerUnavailable { + step: unavailable_step, + detail: join([ + unavailable_why, + ", and the toolchain for host ", instance.host_identity as String, + " declares no systemd-run", + ], ""), + } + Present { value: program } => + DispatchSessionContainerReady { + container: SystemdUnitContainer { + plan: DispatchWorkerUnitPlan { + systemd_run_program: program, + unit: dispatch_attempt_unit_name(node_id: node_id, attempt_key: attempt_key), + working_directory: pane_working_directory, + stdout_path: stdout_path, + stderr_path: stderr_path, + inner_argv: provider_inner, + }, + }, + } + } +} + fn dispatch_session_container_for_provider( provider: DispatchCliProvider, instance: HostDashboardInstance, @@ -2757,39 +2826,29 @@ fn dispatch_session_container_for_provider( ) -> DispatchSessionContainerPlan { match provider { GunbcHarnessProvider => - match instance.toolchain.systemd_run { - Absent => - DispatchSessionContainerUnavailable { - step: "harness-transient-unit" as NonEmptyStr, - detail: join([ - "the gunbc harness worker requires a transient unit for an enforceable memory bound, ", - "and the toolchain for host ", instance.host_identity as String, - " declares no systemd-run", - ], ""), - } - Present { value: program } => - DispatchSessionContainerReady { - container: SystemdUnitContainer { - plan: DispatchWorkerUnitPlan { - systemd_run_program: program, - unit: dispatch_attempt_unit_name(node_id: node_id, attempt_key: attempt_key), - working_directory: pane_working_directory, - worker_log_path: worker_log_path, - inner_argv: provider_inner, - }, - }, - } - } + dispatch_systemd_unit_container_plan( + instance: instance, + node_id: node_id, + attempt_key: attempt_key, + pane_working_directory: pane_working_directory, + stdout_path: worker_log_path, + stderr_path: worker_log_path, + provider_inner: provider_inner, + unavailable_step: "harness-transient-unit" as NonEmptyStr, + unavailable_why: "the gunbc harness worker requires a transient unit for an enforceable memory bound", + ) ClaudeCodeProvider => - DispatchSessionContainerReady { - container: dispatch_tmux_container( - instance: instance, - session_name: session_name, - tmux_spawn: tmux_spawn, - pipe_command: pipe_command, - provider_inner: provider_inner, - ), - } + dispatch_systemd_unit_container_plan( + instance: instance, + node_id: node_id, + attempt_key: attempt_key, + pane_working_directory: pane_working_directory, + stdout_path: events_path, + stderr_path: worker_log_path, + provider_inner: provider_inner, + unavailable_step: "claude-transient-unit" as NonEmptyStr, + unavailable_why: "headless Claude Code requires a transient unit so stream-json stdout is a unit property and the session placement grant is the cgroup bound", + ) CodexDispatchProvider => DispatchSessionContainerReady { container: dispatch_tmux_container( diff --git a/dag/gunbc/roadmap/roadmap_provider_events.dag b/dag/gunbc/roadmap/roadmap_provider_events.dag index aa3ecbc2d71..55cb8ec6188 100644 --- a/dag/gunbc/roadmap/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap/roadmap_provider_events.dag @@ -24,6 +24,16 @@ import extdeps.languages.json.parse { json_text_parseable, parse_json_document, JsonDocumentParse, JsonDocumentParsed, JsonDocumentUnreadable, json_field_string, json_field_int, JsonFieldRead, FieldRead, FieldAbsent, FieldMalformed, } +import extdeps.llm.claude_code_stream_json { + ClaudeCodeStreamLine, + ClaudeCodeRateLimitEventLine, + ClaudeCodeResultLine, + ClaudeCodeSystemLine, + ClaudeCodeAssistantLine, + ClaudeCodeUserLine, + ClaudeCodeOtherLine, + ClaudeCodeLineUnreadable, +} // THE HARNESS ARMS ARE THE READ SIDE OF gunbc.harness.harness_tool_kind. A harness tool use is // written with an item type derived from its HarnessTool, and codex_item_activity inverts that same @@ -160,7 +170,7 @@ data codex_provider_event_projection_filter: NonEmptyStr = join([ to_string(character_count_value(c: codex_provider_projection_message_budget)), "]),\"truncated\":((.item.text | length) > ", to_string(character_count_value(c: codex_provider_projection_message_budget)), - ")} else null end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then (if item_type == \"agent_message\" and (agent_report != null) then {\"type\":$type,\"item\":{\"type\":item_type,\"message\":agent_report}} else {\"type\":$type,\"item\":{\"type\":item_type}} end) elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"turn.budget_exhausted\" then {\"type\":$type,\"steps_taken\":(.steps_taken | num)} elif $type == \"turn.placement_waiting\" then {\"type\":$type,\"wait\":(.wait | num),\"wait_cap\":(.wait_cap | num),\"reason\":(.reason | bounded_text)} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} elif $type == \"round.usage\" then ({\"type\":$type,\"step\":(.step | num),\"input_tokens\":(.input_tokens | num),\"output_tokens\":(.output_tokens | num),\"epoch_ms\":(.epoch_ms | num)} + (if (.files_changed | type) == \"number\" then {\"files_changed\":.files_changed} else {} end) + (if (.tool_calls | type) == \"number\" then {\"tool_calls\":.tool_calls} else {} end)) else {\"type\":$type} end", + ")} else null end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then (if item_type == \"agent_message\" and (agent_report != null) then {\"type\":$type,\"item\":{\"type\":item_type,\"message\":agent_report}} else {\"type\":$type,\"item\":{\"type\":item_type}} end) elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"turn.budget_exhausted\" then {\"type\":$type,\"steps_taken\":(.steps_taken | num)} elif $type == \"turn.placement_waiting\" then {\"type\":$type,\"wait\":(.wait | num),\"wait_cap\":(.wait_cap | num),\"reason\":(.reason | bounded_text)} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} elif $type == \"round.usage\" then ({\"type\":$type,\"step\":(.step | num),\"input_tokens\":(.input_tokens | num),\"output_tokens\":(.output_tokens | num),\"epoch_ms\":(.epoch_ms | num)} + (if (.files_changed | type) == \"number\" then {\"files_changed\":.files_changed} else {} end) + (if (.tool_calls | type) == \"number\" then {\"tool_calls\":.tool_calls} else {} end)) elif $type == \"result\" then (if .is_error == true then {\"type\":\"turn.failed\",\"error\":{\"message\":((.result // .) | bounded_text)}} else {\"type\":\"turn.completed\"} end) elif $type == \"system\" then {\"type\":\"thread.started\"} elif $type == \"assistant\" then (if (.message.content | type) == \"array\" and (any(.message.content[]; (.type | type) == \"string\" and .type == \"tool_use\")) then {\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\"}} else {\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\"}} end) elif $type == \"user\" then (if (.message.content | type) == \"array\" and (any(.message.content[]; (.type | type) == \"string\" and .type == \"tool_result\")) then {\"type\":\"item.completed\",\"item\":{\"type\":\"command_execution\"}} else empty end) elif $type == \"rate_limit_event\" then empty else {\"type\":$type} end", ], "") as NonEmptyStr fn codex_provider_event_projection_args(path: FilePath) -> List { @@ -172,6 +182,37 @@ fn codex_provider_event_projection_args(path: FilePath) -> List { ] } +fn claude_provider_event_projection_args(path: FilePath) -> List { + codex_provider_event_projection_args(path: path) +} + +fn claude_code_line_codex_kind(line: ClaudeCodeStreamLine) -> CodexProviderEventKind? { + match line { + ClaudeCodeSystemLine => Present { value: CodexThreadStarted } + ClaudeCodeAssistantLine { has_tool_use } => + if has_tool_use { + Present { value: CodexItemStarted { activity: CodexRunningCommand } } + } else { + Present { value: CodexItemCompleted { activity: CodexReporting } } + } + ClaudeCodeUserLine { has_tool_result } => + if has_tool_result { + Present { value: CodexItemCompleted { activity: CodexRunningCommand } } + } else { + none + } + ClaudeCodeResultLine { result } => + if result.is_error { + Present { value: CodexTurnFailed } + } else { + Present { value: CodexTurnCompleted } + } + ClaudeCodeRateLimitEventLine { info: _ } => none + ClaudeCodeOtherLine { event_type: _ } => none + ClaudeCodeLineUnreadable { gap: _ } => none + } +} + // The attempt-owned provider-events.jsonl remains the complete Codex protocol authority. Workflow // observation first runs a separately modeled jq capability over that file. `--raw-input` plus // `fromjson` proves exactly one JSON value per physical JSONL line before emitting only a bounded @@ -183,7 +224,7 @@ fn codex_provider_event_projection_args(path: FilePath) -> List { // visible as an observation refusal for retained attempts. A malformed raw event makes jq nonzero, // so no parsed prefix is accepted as a complete observation. -data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review 44058, DESIGN 6). codex_provider_event_projection_filter is a jq PROGRAM assembled by join — a foreign language built as a string, the same class the tmux pipe payload was in before it moved onto the bash backend (gunbc.dispatch_pipe_pane_emit). It is marked rather than fixed because the two available routes are not equivalent: minting a jq grammar to read backward would buy a modeled emitter for a filter that exists ONLY to bound a line before the substrate reads it, which is the purity trap DESIGN 6 names — the displaced cost is zero once the read itself is bounded. DISSOLVES ON: the observation reading the attempt JSONL directly under a substrate-side line bound instead of shelling to a projector — the witness-realization lane's typed file read (docs/plans/witness-realization-plan.md), at which point BOTH the filter string and the ProviderEventProjectionCapability jq dependency delete together. Until then the interpolated values are the two bounds above (both Int data rows), never caller input, and the classifier downstream is anchored and total, so a projector drift refuses rather than reclassifying." +data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review 44058, DESIGN 6). codex_provider_event_projection_filter is a jq PROGRAM assembled by join — a foreign language built as a string, the same class the tmux pipe payload was in before it moved onto the bash backend (gunbc.dispatch_pipe_pane_emit). It is marked rather than fixed because the two available routes are not equivalent: minting a jq grammar to read backward would buy a modeled emitter for a filter that exists ONLY to bound a line before the substrate reads it, which is the purity trap DESIGN 6 names — the displaced cost is zero once the read itself is bounded. DISSOLVES ON: the observation reading the attempt JSONL directly under a substrate-side line bound instead of shelling to a projector — the witness-realization lane's typed file read (docs/plans/witness-realization-plan.md), at which point BOTH the filter string and the ProviderEventProjectionCapability jq dependency delete together. Until then the interpolated values are the two bounds above (both Int data rows), never caller input, and the classifier downstream is anchored and total, so a projector drift refuses rather than reclassifying. Claude Code stream-json (system, assistant tool_use, user tool_result, result, rate_limit_event) is mapped into the same Codex envelope here because the belt observes one events file with no provider discriminator; claude_code_line_codex_kind is the typed authority for that mapping and the jq is its projector." // Codex --json is the provider protocol authority: one JSON object per line with thread.started, // turn.started, item.*, turn.completed, turn.budget_exhausted, turn.failed, and error event types, diff --git a/dag/test/claim/claude_code_dispatch_witness_test.dag b/dag/test/claim/claude_code_dispatch_witness_test.dag index 77b9d605a4d..0da2d2f5cea 100644 --- a/dag/test/claim/claude_code_dispatch_witness_test.dag +++ b/dag/test/claim/claude_code_dispatch_witness_test.dag @@ -11,6 +11,9 @@ import extdeps.llm.claude_code_stream_json { ClaudeCodeRateLimitEventLine, ClaudeCodeResultLine, ClaudeCodeOtherLine, + ClaudeCodeSystemLine, + ClaudeCodeAssistantLine, + ClaudeCodeUserLine, ClaudeCodeLineUnreadable, StreamLineNotJson, StreamLineMemberMissing, @@ -167,6 +170,9 @@ test fn the_captured_rate_limit_line_decodes_both_windows_in_basis_points() -> B && any(info.windows, w => w.name as String == "seven_day" && basis_point_count(bp: w.utilization) == 4500) ClaudeCodeResultLine { result: _ } => false ClaudeCodeOtherLine { event_type: _ } => false + ClaudeCodeSystemLine => false + ClaudeCodeAssistantLine { has_tool_use: _ } => false + ClaudeCodeUserLine { has_tool_result: _ } => false ClaudeCodeLineUnreadable { gap: _ } => false } } @@ -182,6 +188,9 @@ test fn the_unauthorized_result_carries_is_error_and_numeric_status() -> Bool { } ClaudeCodeRateLimitEventLine { info: _ } => false ClaudeCodeOtherLine { event_type: _ } => false + ClaudeCodeSystemLine => false + ClaudeCodeAssistantLine { has_tool_use: _ } => false + ClaudeCodeUserLine { has_tool_result: _ } => false ClaudeCodeLineUnreadable { gap: _ } => false } } @@ -197,6 +206,9 @@ test fn a_string_utilization_refuses_naming_the_member() -> Bool { ClaudeCodeRateLimitEventLine { info: _ } => false ClaudeCodeResultLine { result: _ } => false ClaudeCodeOtherLine { event_type: _ } => false + ClaudeCodeSystemLine => false + ClaudeCodeAssistantLine { has_tool_use: _ } => false + ClaudeCodeUserLine { has_tool_result: _ } => false } } @@ -217,6 +229,9 @@ fn refuses_naming(line: String, member: String) -> Bool { ClaudeCodeRateLimitEventLine { info: _ } => false ClaudeCodeResultLine { result: _ } => false ClaudeCodeOtherLine { event_type: _ } => false + ClaudeCodeSystemLine => false + ClaudeCodeAssistantLine { has_tool_use: _ } => false + ClaudeCodeUserLine { has_tool_result: _ } => false } } diff --git a/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag b/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag index 017f1256bbb..fc5b74f8438 100644 --- a/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag +++ b/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag @@ -6,6 +6,9 @@ import extdeps.llm.cli { claude_trust_store_path, claude_workspace_trust_seeded, shape_claude_invoke_argv, + shape_claude_print_argv, + ClaudePrintUnboundedTurns, + ClaudePrintMaxTurns, ProviderAccountDefaultModel, ReasoningHigh, } @@ -78,3 +81,30 @@ test fn w_argv_head_is_program_authority() -> Bool { } } +test fn w_print_argv_is_stream_json_headless() -> Bool { + let argv = shape_claude_print_argv( + append_system_prompt: "brief", + start_prompt: "go", + permission_args: ["--dangerously-skip-permissions"], + model: ProviderAccountDefaultModel, + turn_bound: ClaudePrintMaxTurns { turns: 1 }, + ) + let wire = join(argv, separator: "\0") + match argv.first() { + Present { value: head } => head == claude_cli_program + Absent => false + } + && string_contains(s: wire, pattern: "\0-p\0--output-format\0stream-json\0--verbose") + && string_contains(s: wire, pattern: "\0--max-turns\01\0") + && string_contains(s: wire, pattern: "\0--append-system-prompt\0brief\0") + && string_contains(s: wire, pattern: "\0--dangerously-skip-permissions\0go") + && !string_contains(s: wire, pattern: "--session-id") + && !string_contains(s: join(shape_claude_print_argv( + append_system_prompt: "brief", + start_prompt: "go", + permission_args: [], + model: ProviderAccountDefaultModel, + turn_bound: ClaudePrintUnboundedTurns, + ), separator: "\0"), pattern: "--max-turns") +} + diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index dcccbf697a5..aff2dfddfab 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -45,6 +45,7 @@ import extdeps.tmux { AttemptPanesParsed, AttemptPanesParseRefused, parse_tmux_attempt_panes_output, + TmuxPipeCommand, } import extdeps.llm.cli { shape_claude_invoke_argv, @@ -63,6 +64,9 @@ import gunbc.roadmap_dispatch_actuator { dispatch_supervisor_unit_command, dispatch_attempt_audit_events_path_for_instance, dispatch_attempt_supervisor_events_path_for_instance, + dispatch_attempt_events_path_for_instance, + dispatch_attempt_worker_log_path_for_instance, + dispatch_attempt_unit_name, attempt_state_prepare_argv_for_instance, dispatch_reviewer_attempt_identity, dispatch_attempt_publication_dir_for_instance, @@ -106,6 +110,9 @@ import gunbc.roadmap_dispatch_actuator { HostExecArgv, SystemdUnitContainer, TmuxSessionContainer, + dispatch_session_container_for_provider, + DispatchSessionContainerReady, + DispatchSessionContainerUnavailable, DispatchSpawnReady, DispatchSpawnRefused, git_worktree_add_argv, @@ -296,15 +303,13 @@ test fn witness_dispatch_effort_ignores_intricacy() -> Bool { test fn witness_claude_argv_uses_dangerous_skip_permissions() -> Bool { let argv = claude_spawn_argv( - effort: ReasoningHigh, model: ProviderAccountDefaultModel, - session_uuid: "uuid-1", - node_id: roadmap_dispatch_actuator_nid(s: "node-1"), brief: "brief body", ) string_contains(s: join(argv, separator: "\0"), pattern: "--dangerously-skip-permissions") - && string_contains(s: join(argv, separator: "\0"), pattern: "skipDangerousModePermissionPrompt") - && string_contains(s: join(argv, separator: "\0"), pattern: "--session-id") + && string_contains(s: join(argv, separator: "\0"), pattern: "-p") + && string_contains(s: join(argv, separator: "\0"), pattern: "stream-json") + && !string_contains(s: join(argv, separator: "\0"), pattern: "--session-id") && !string_contains(s: join(argv, separator: "\0"), pattern: "--resume") } @@ -320,10 +325,7 @@ test fn witness_git_worktree_argv_is_execfile_safe() -> Bool { test fn witness_tmux_spawn_argv_threads_claude_argv() -> Bool { let claude = claude_spawn_argv( - effort: ReasoningMedium, model: ProviderAccountDefaultModel, - session_uuid: "uuid-2", - node_id: roadmap_dispatch_actuator_nid(s: "node-2"), brief: "brief", ) let cmd = tmux_spawn_argv( @@ -906,10 +908,7 @@ test fn witness_host_exec_argv_shapes_bound_to_extdeps() -> Bool { test fn witness_claude_invoke_permission_args_are_gunbc_policy() -> Bool { let argv = claude_spawn_argv( - effort: ReasoningHigh, model: ProviderAccountDefaultModel, - session_uuid: "uuid-policy", - node_id: roadmap_dispatch_actuator_nid(s: "node-policy"), brief: "brief", ) string_contains(s: join(argv, separator: "\0"), pattern: "--dangerously-skip-permissions") @@ -1055,10 +1054,7 @@ data witness_pinned_dispatch_model: NonEmptyStr = "claude-opus-4-1-20250805" as test fn witness_pinned_model_reaches_claude_argv() -> Bool { let argv = claude_spawn_argv( - effort: ReasoningHigh, model: ProviderModelPinned { model: witness_pinned_dispatch_model }, - session_uuid: "uuid-pin", - node_id: roadmap_dispatch_actuator_nid(s: "node-pin"), brief: "brief", ) string_contains( @@ -1069,10 +1065,7 @@ test fn witness_pinned_model_reaches_claude_argv() -> Bool { test fn witness_claude_account_default_passes_no_model_flag() -> Bool { let argv = claude_spawn_argv( - effort: ReasoningHigh, model: ProviderAccountDefaultModel, - session_uuid: "uuid-default", - node_id: roadmap_dispatch_actuator_nid(s: "node-default"), brief: "brief", ) !string_contains(s: join(argv, separator: "\0"), pattern: "--model") @@ -1453,3 +1446,43 @@ test fn witness_supervisor_launch_writes_the_events_path_metering_reads() -> Boo ) } } + +test fn witness_headless_claude_uses_a_transient_unit_with_stream_json_on_the_events_path() -> Bool { + let instance = srv1_live_dashboard_instance() + let node_id = "claude-headless" + let attempt_key = "feedfacefeedface" + let events = dispatch_attempt_events_path_for_instance( + instance: instance, + node_id: roadmap_dispatch_actuator_nid(s: node_id), + attempt_key: attempt_key, + ) + let log = dispatch_attempt_worker_log_path_for_instance( + instance: instance, + node_id: roadmap_dispatch_actuator_nid(s: node_id), + attempt_key: attempt_key, + ) + match dispatch_session_container_for_provider( + provider: ClaudeCodeProvider, + instance: instance, + node_id: node_id, + attempt_key: attempt_key, + session_name: "unused", + events_path: events, + worker_log_path: log, + pane_working_directory: "/wt", + tmux_spawn: HostExecArgv { program: "tmux", args: [] }, + pipe_command: TmuxPipeCommand { body: "true" as NonEmptyStr }, + provider_inner: ["claude", "-p"], + ) { + DispatchSessionContainerUnavailable { step: _, detail: _ } => false + DispatchSessionContainerReady { container } => + match container { + TmuxSessionContainer { spawn: _, default_shell: _, remain_on_exit: _, event_pipe: _, provider_start: _ } => false + SystemdUnitContainer { plan } => + plan.unit == dispatch_attempt_unit_name(node_id: node_id, attempt_key: attempt_key) + && plan.working_directory == "/wt" + && plan.stdout_path == events + && plan.stderr_path == log + } + } +} diff --git a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag index a75a6e4ef63..ec92e843acc 100644 --- a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag @@ -1,6 +1,6 @@ module test.claim.roadmap_provider_events_witness_test -import std.types { Bool, String, Int, List } +import std.types { Bool, String, Int, List, FilePath } import extdeps.languages.json.emit { json_object } import gunbc.harness.harness_wire { WireToolUseBlock } import gunbc.harness.harness_tool_kind { @@ -8,6 +8,8 @@ import gunbc.harness.harness_tool_kind { } import std.measure { character_count, character_count_value } import gunbc.harness.harness_turn { harness_event_placement_waiting, harness_tool_round_events } +import extdeps.llm.claude_code_stream_json { claude_code_stream_line } +import std.optional { Present, Absent } import gunbc.roadmap_provider_events { CodexItemActivity, CodexRunningCommand, CodexEditingFiles, CodexUsingConnectedTool, CodexSearching, CodexUpdatingPlan, CodexReasoningItem, CodexReporting, HarnessReadingFile, HarnessWritingFile, @@ -23,6 +25,14 @@ import gunbc.roadmap_provider_events { ProviderEventsRefused, parse_codex_jsonl, parse_codex_jsonl_with_line_budget, + claude_code_line_codex_kind, + claude_provider_event_projection_args, + codex_provider_event_projection_args, + CodexThreadStarted, + CodexTurnCompleted, + CodexTurnFailed, + CodexItemStarted, + CodexRunningCommand, codex_provider_event_line_observation_budget, codex_provider_projection_type_budget, codex_provider_projection_detail_budget, @@ -436,6 +446,55 @@ test fn an_undeclared_tool_round_is_read_back_as_undeclared_not_as_a_command() - tool_use_round_tags(name: "delete_everything") == "started:undeclared,completed:undeclared" } +test fn claude_stream_json_projects_into_the_codex_event_kinds_the_belt_reads() -> Bool { + let system_ok = match claude_code_line_codex_kind(line: claude_code_stream_line(line: "{\"type\":\"system\",\"subtype\":\"init\"}")) { + Present { value: kind } => + match kind { + CodexThreadStarted => true + _ => false + } + Absent => false + } + let tool_ok = match claude_code_line_codex_kind(line: claude_code_stream_line(line: "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\",\"name\":\"Bash\"}]}}")) { + Present { value: kind } => + match kind { + CodexItemStarted { activity } => + match activity { + CodexRunningCommand => true + _ => false + } + _ => false + } + Absent => false + } + let result_ok = match claude_code_line_codex_kind(line: claude_code_stream_line(line: "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false}")) { + Present { value: kind } => + match kind { + CodexTurnCompleted => true + _ => false + } + Absent => false + } + let fail_ok = match claude_code_line_codex_kind(line: claude_code_stream_line(line: "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":true,\"api_error_status\":401}")) { + Present { value: kind } => + match kind { + CodexTurnFailed => true + _ => false + } + Absent => false + } + let rate_skip = match claude_code_line_codex_kind(line: claude_code_stream_line(line: "{\"type\":\"rate_limit_event\",\"rate_limit_info\":{\"status\":\"allowed\",\"unifiedWindows\":{}}}")) { + Absent => true + Present { value: _ } => false + } + let filter = codex_provider_event_projection_filter as String + system_ok && tool_ok && result_ok && fail_ok && rate_skip + && string_contains(s: filter, pattern: "$type == \"result\"") + && string_contains(s: filter, pattern: "$type == \"system\"") + && string_contains(s: filter, pattern: "rate_limit_event") + && claude_provider_event_projection_args(path: "/tmp/e.jsonl" as FilePath) == codex_provider_event_projection_args(path: "/tmp/e.jsonl" as FilePath) +} + test fn a_harness_tool_round_reaches_the_operator_as_its_own_activity() -> Bool { let read_line = concat(join(harness_tool_round_events( uses: [WireToolUseBlock { id: "toolu_1", name: harness_tool_wire_name(tool: ReadFileTool) as String, input: json_object([]) }], From 4a70d3251bd62e35b110b2faf72b6ea66a6ae1e6 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 22:17:05 +0000 Subject: [PATCH 2/3] Address review 78387: one Claude event mapping, explicit executor, transmit effort. parse_codex_jsonl now classifies bounded Claude stream-json via claude_code_line_codex_kind; jq only bounds those lines. ExecutorDefault stays a harness refusal. Print argv carries --effort. Co-authored-by: Cursor --- dag/extdeps/llm/cli.dag | 14 +++++-- .../roadmap/roadmap_dispatch_actuator.dag | 38 +++++-------------- dag/gunbc/roadmap/roadmap_provider_events.dag | 23 +++++++---- .../extdeps_llm_claude_trust_witness_test.dag | 3 ++ ...roadmap_dispatch_actuator_witness_test.dag | 6 +++ .../roadmap_provider_events_witness_test.dag | 18 ++++++--- 6 files changed, 58 insertions(+), 44 deletions(-) diff --git a/dag/extdeps/llm/cli.dag b/dag/extdeps/llm/cli.dag index ce2923d3f04..5f72c831235 100644 --- a/dag/extdeps/llm/cli.dag +++ b/dag/extdeps/llm/cli.dag @@ -219,6 +219,8 @@ data claude_cli_max_turns_flag: String = "--max-turns" data claude_cli_append_system_prompt_flag: String = "--append-system-prompt" +data claude_cli_effort_flag: String = "--effort" + fn claude_print_max_turns_args(bound: ClaudePrintTurnBound) -> List { match bound { ClaudePrintUnboundedTurns => [] @@ -231,6 +233,7 @@ fn shape_claude_print_argv( start_prompt: String, permission_args: List, model: ProviderModelSelection, + effort: ReasoningEffort, turn_bound: ClaudePrintTurnBound, ) -> List { concat( @@ -240,10 +243,13 @@ fn shape_claude_print_argv( concat( claude_print_max_turns_args(bound: turn_bound), concat( - [claude_cli_append_system_prompt_flag, append_system_prompt], + [claude_cli_effort_flag, reasoning_effort_label(effort: effort)], concat( - claude_model_args(selection: model), - concat(permission_args, [start_prompt]), + [claude_cli_append_system_prompt_flag, append_system_prompt], + concat( + claude_model_args(selection: model), + concat(permission_args, [start_prompt]), + ), ), ), ), @@ -276,7 +282,7 @@ fn shape_claude_invoke_argv( claude_model_args(selection: model), [ "--settings", settings_json, - "--effort", reasoning_effort_label(effort: effort), + claude_cli_effort_flag, reasoning_effort_label(effort: effort), "--session-id", session_id, "-n", node_id, "--append-system-prompt", append_system_prompt, diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index 85f2d7ba93c..4ea0e5fd1e5 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -465,15 +465,11 @@ fn dispatch_actuator_selection_for_provider_on( // per-window usage limits) does not admit the draw, refuses typed, and is never replaced by the // harness. Absence of a request is ExecutorDefault, never a vendor. // -// AUTOMATIC RANKING BETWEEN HARNESS AND VENDOR REMAINS A FRONTIER (std.decision select_realization -// over funded axes: cost, usage-window headroom, latency, quality). That trigger is unchanged: a -// grounded funded-axis policy once those readings exist for at least two providers. -// -// WHAT THIS FUNCTION DOES LIFT is not that ranking. ExecutorDefault still prefers a serving harness -// route. When the harness has no enrolled serving route AND the Claude custody file is present on -// this host, the default admits the same observed-Claude path an explicit `?executor=claude` already -// takes. Absence of custody keeps the harness refusal. That is fail-closed admission of the only -// remaining executing backend, not a Pareto choice among two live ones. +// DECLARED FRONTIER, NOT CODE: automatic selection between the harness and a vendor is a std.decision +// select_realization over funded axes (cost, usage-window headroom, latency, quality). Its trigger is +// a grounded funded-axis policy once limit, latency and quality readings exist for at least two +// providers; until then nothing here chooses on the operator's behalf. A harness refusal therefore +// stays a refusal. Headless Claude is reached by `?executor=claude`, not by widening the default. type DispatchExecutorRequest = ExecutorDefault | ExecutorVendor { provider: DispatchCliProvider } @@ -683,25 +679,7 @@ fn dispatch_actuator_selection_for_request( sizing_expectation: SizingProfileExpectation, ) -> DispatchActuatorSelection { match request { - ExecutorDefault => - match dispatch_harness_selection() { - DispatchActuatorSelectionOk { provider, effort, model, process_fingerprint } => - DispatchActuatorSelectionOk { - provider: provider, - effort: effort, - model: model, - process_fingerprint: process_fingerprint, - } - DispatchActuatorSelectionRefused { reason: harness_reason } => - match claude_credential_custody_source() { - CredentialSnapshotTaken { path: _ } => - dispatch_actuator_claude_selection_observed( - instance: instance, node_id: node_id, attempt_key: attempt_key, sizing_expectation: sizing_expectation, - ) - CredentialSnapshotRefused { reason: _ } => - DispatchActuatorSelectionRefused { reason: harness_reason } - } - } + ExecutorDefault => dispatch_actuator_selection(sizing_expectation: sizing_expectation) ExecutorVendor { provider } => match provider { ClaudeCodeProvider => @@ -2218,6 +2196,7 @@ fn git_worktree_add_detached_argv_for_instance( } fn claude_spawn_argv( + effort: ReasoningEffort, model: ProviderModelSelection, brief: String, ) -> List { @@ -2226,6 +2205,7 @@ fn claude_spawn_argv( start_prompt: dispatch_claude_start_prompt, permission_args: dispatch_claude_permission_args, model: model, + effort: effort, turn_bound: ClaudePrintUnboundedTurns, ) } @@ -2351,6 +2331,7 @@ fn dispatch_provider_inner_argv( argv: concat( [env_path_resolved_program().invocation as String, "CLAUDE_CODE_NO_FLICKER=1"], claude_spawn_argv( + effort: effort, model: model, brief: brief, ), @@ -2431,6 +2412,7 @@ fn dispatch_provider_inner_argv_for_instance( "CLAUDE_CODE_NO_FLICKER=1", ], claude_spawn_argv( + effort: effort, model: model, brief: brief, ), diff --git a/dag/gunbc/roadmap/roadmap_provider_events.dag b/dag/gunbc/roadmap/roadmap_provider_events.dag index 55cb8ec6188..0bd82f6da62 100644 --- a/dag/gunbc/roadmap/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap/roadmap_provider_events.dag @@ -33,6 +33,7 @@ import extdeps.llm.claude_code_stream_json { ClaudeCodeUserLine, ClaudeCodeOtherLine, ClaudeCodeLineUnreadable, + claude_code_stream_line, } // THE HARNESS ARMS ARE THE READ SIDE OF gunbc.harness.harness_tool_kind. A harness tool use is @@ -170,7 +171,11 @@ data codex_provider_event_projection_filter: NonEmptyStr = join([ to_string(character_count_value(c: codex_provider_projection_message_budget)), "]),\"truncated\":((.item.text | length) > ", to_string(character_count_value(c: codex_provider_projection_message_budget)), - ")} else null end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then (if item_type == \"agent_message\" and (agent_report != null) then {\"type\":$type,\"item\":{\"type\":item_type,\"message\":agent_report}} else {\"type\":$type,\"item\":{\"type\":item_type}} end) elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"turn.budget_exhausted\" then {\"type\":$type,\"steps_taken\":(.steps_taken | num)} elif $type == \"turn.placement_waiting\" then {\"type\":$type,\"wait\":(.wait | num),\"wait_cap\":(.wait_cap | num),\"reason\":(.reason | bounded_text)} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} elif $type == \"round.usage\" then ({\"type\":$type,\"step\":(.step | num),\"input_tokens\":(.input_tokens | num),\"output_tokens\":(.output_tokens | num),\"epoch_ms\":(.epoch_ms | num)} + (if (.files_changed | type) == \"number\" then {\"files_changed\":.files_changed} else {} end) + (if (.tool_calls | type) == \"number\" then {\"tool_calls\":.tool_calls} else {} end)) elif $type == \"result\" then (if .is_error == true then {\"type\":\"turn.failed\",\"error\":{\"message\":((.result // .) | bounded_text)}} else {\"type\":\"turn.completed\"} end) elif $type == \"system\" then {\"type\":\"thread.started\"} elif $type == \"assistant\" then (if (.message.content | type) == \"array\" and (any(.message.content[]; (.type | type) == \"string\" and .type == \"tool_use\")) then {\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\"}} else {\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\"}} end) elif $type == \"user\" then (if (.message.content | type) == \"array\" and (any(.message.content[]; (.type | type) == \"string\" and .type == \"tool_result\")) then {\"type\":\"item.completed\",\"item\":{\"type\":\"command_execution\"}} else empty end) elif $type == \"rate_limit_event\" then empty else {\"type\":$type} end", + ")} else null end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then (if item_type == \"agent_message\" and (agent_report != null) then {\"type\":$type,\"item\":{\"type\":item_type,\"message\":agent_report}} else {\"type\":$type,\"item\":{\"type\":item_type}} end) elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"turn.budget_exhausted\" then {\"type\":$type,\"steps_taken\":(.steps_taken | num)} elif $type == \"turn.placement_waiting\" then {\"type\":$type,\"wait\":(.wait | num),\"wait_cap\":(.wait_cap | num),\"reason\":(.reason | bounded_text)} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} elif $type == \"round.usage\" then ({\"type\":$type,\"step\":(.step | num),\"input_tokens\":(.input_tokens | num),\"output_tokens\":(.output_tokens | num),\"epoch_ms\":(.epoch_ms | num)} + (if (.files_changed | type) == \"number\" then {\"files_changed\":.files_changed} else {} end) + (if (.tool_calls | type) == \"number\" then {\"tool_calls\":.tool_calls} else {} end)) elif $type == \"result\" then {\"type\":$type,\"subtype\":(if (.subtype | type) == \"string\" then .subtype[0:", + to_string(character_count_value(c: codex_provider_projection_type_budget)), + "] else \"unknown\" end),\"is_error\":(if (.is_error | type) == \"boolean\" then .is_error else true end)} elif $type == \"system\" then {\"type\":$type} elif $type == \"assistant\" or $type == \"user\" then {\"type\":$type,\"message\":{\"content\":(if (.message.content | type) == \"array\" then [.message.content[] | {\"type\":(if (.type | type) == \"string\" then .type[0:", + to_string(character_count_value(c: codex_provider_projection_type_budget)), + "] else \"unknown\" end)}] else [] end)}} elif $type == \"rate_limit_event\" then empty else {\"type\":$type} end", ], "") as NonEmptyStr fn codex_provider_event_projection_args(path: FilePath) -> List { @@ -182,10 +187,6 @@ fn codex_provider_event_projection_args(path: FilePath) -> List { ] } -fn claude_provider_event_projection_args(path: FilePath) -> List { - codex_provider_event_projection_args(path: path) -} - fn claude_code_line_codex_kind(line: ClaudeCodeStreamLine) -> CodexProviderEventKind? { match line { ClaudeCodeSystemLine => Present { value: CodexThreadStarted } @@ -224,7 +225,7 @@ fn claude_code_line_codex_kind(line: ClaudeCodeStreamLine) -> CodexProviderEvent // visible as an observation refusal for retained attempts. A malformed raw event makes jq nonzero, // so no parsed prefix is accepted as a complete observation. -data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review 44058, DESIGN 6). codex_provider_event_projection_filter is a jq PROGRAM assembled by join — a foreign language built as a string, the same class the tmux pipe payload was in before it moved onto the bash backend (gunbc.dispatch_pipe_pane_emit). It is marked rather than fixed because the two available routes are not equivalent: minting a jq grammar to read backward would buy a modeled emitter for a filter that exists ONLY to bound a line before the substrate reads it, which is the purity trap DESIGN 6 names — the displaced cost is zero once the read itself is bounded. DISSOLVES ON: the observation reading the attempt JSONL directly under a substrate-side line bound instead of shelling to a projector — the witness-realization lane's typed file read (docs/plans/witness-realization-plan.md), at which point BOTH the filter string and the ProviderEventProjectionCapability jq dependency delete together. Until then the interpolated values are the two bounds above (both Int data rows), never caller input, and the classifier downstream is anchored and total, so a projector drift refuses rather than reclassifying. Claude Code stream-json (system, assistant tool_use, user tool_result, result, rate_limit_event) is mapped into the same Codex envelope here because the belt observes one events file with no provider discriminator; claude_code_line_codex_kind is the typed authority for that mapping and the jq is its projector." +data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review 44058, DESIGN 6). codex_provider_event_projection_filter is a jq PROGRAM assembled by join — a foreign language built as a string, the same class the tmux pipe payload was in before it moved onto the bash backend (gunbc.dispatch_pipe_pane_emit). It is marked rather than fixed because the two available routes are not equivalent: minting a jq grammar to read backward would buy a modeled emitter for a filter that exists ONLY to bound a line before the substrate reads it, which is the purity trap DESIGN 6 names — the displaced cost is zero once the read itself is bounded. DISSOLVES ON: the observation reading the attempt JSONL directly under a substrate-side line bound instead of shelling to a projector — the witness-realization lane's typed file read (docs/plans/witness-realization-plan.md), at which point BOTH the filter string and the ProviderEventProjectionCapability jq dependency delete together. Until then the interpolated values are the two bounds above (both Int data rows), never caller input, and the classifier downstream is anchored and total, so a projector drift refuses rather than reclassifying. Claude Code stream-json is bounded here (type, is_error, content-block types) and classified by claude_code_line_codex_kind from parse_codex_jsonl; the jq does not mint Codex event types for Claude lines." // Codex --json is the provider protocol authority: one JSON object per line with thread.started, // turn.started, item.*, turn.completed, turn.budget_exhausted, turn.failed, and error event types, @@ -508,7 +509,15 @@ fn codex_event_from_complete_line( kind: CodexError, } } } else { - none + match claude_code_line_codex_kind(line: claude_code_stream_line(line: raw)) { + Present { value: kind } => + Present { value: CodexProviderEvent { + line_number: line_number, + raw: raw, + kind: kind, + } } + Absent => none + } } } diff --git a/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag b/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag index fc5b74f8438..2fd3438724c 100644 --- a/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag +++ b/dag/test/claim/extdeps_llm_claude_trust_witness_test.dag @@ -87,6 +87,7 @@ test fn w_print_argv_is_stream_json_headless() -> Bool { start_prompt: "go", permission_args: ["--dangerously-skip-permissions"], model: ProviderAccountDefaultModel, + effort: ReasoningHigh, turn_bound: ClaudePrintMaxTurns { turns: 1 }, ) let wire = join(argv, separator: "\0") @@ -96,6 +97,7 @@ test fn w_print_argv_is_stream_json_headless() -> Bool { } && string_contains(s: wire, pattern: "\0-p\0--output-format\0stream-json\0--verbose") && string_contains(s: wire, pattern: "\0--max-turns\01\0") + && string_contains(s: wire, pattern: "\0--effort\0high\0") && string_contains(s: wire, pattern: "\0--append-system-prompt\0brief\0") && string_contains(s: wire, pattern: "\0--dangerously-skip-permissions\0go") && !string_contains(s: wire, pattern: "--session-id") @@ -104,6 +106,7 @@ test fn w_print_argv_is_stream_json_headless() -> Bool { start_prompt: "go", permission_args: [], model: ProviderAccountDefaultModel, + effort: ReasoningHigh, turn_bound: ClaudePrintUnboundedTurns, ), separator: "\0"), pattern: "--max-turns") } diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index aff2dfddfab..c66fecbc0d0 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -303,12 +303,14 @@ test fn witness_dispatch_effort_ignores_intricacy() -> Bool { test fn witness_claude_argv_uses_dangerous_skip_permissions() -> Bool { let argv = claude_spawn_argv( + effort: ReasoningHigh, model: ProviderAccountDefaultModel, brief: "brief body", ) string_contains(s: join(argv, separator: "\0"), pattern: "--dangerously-skip-permissions") && string_contains(s: join(argv, separator: "\0"), pattern: "-p") && string_contains(s: join(argv, separator: "\0"), pattern: "stream-json") + && string_contains(s: join(argv, separator: "\0"), pattern: "--effort") && !string_contains(s: join(argv, separator: "\0"), pattern: "--session-id") && !string_contains(s: join(argv, separator: "\0"), pattern: "--resume") } @@ -325,6 +327,7 @@ test fn witness_git_worktree_argv_is_execfile_safe() -> Bool { test fn witness_tmux_spawn_argv_threads_claude_argv() -> Bool { let claude = claude_spawn_argv( + effort: ReasoningMedium, model: ProviderAccountDefaultModel, brief: "brief", ) @@ -908,6 +911,7 @@ test fn witness_host_exec_argv_shapes_bound_to_extdeps() -> Bool { test fn witness_claude_invoke_permission_args_are_gunbc_policy() -> Bool { let argv = claude_spawn_argv( + effort: ReasoningHigh, model: ProviderAccountDefaultModel, brief: "brief", ) @@ -1054,6 +1058,7 @@ data witness_pinned_dispatch_model: NonEmptyStr = "claude-opus-4-1-20250805" as test fn witness_pinned_model_reaches_claude_argv() -> Bool { let argv = claude_spawn_argv( + effort: ReasoningHigh, model: ProviderModelPinned { model: witness_pinned_dispatch_model }, brief: "brief", ) @@ -1065,6 +1070,7 @@ test fn witness_pinned_model_reaches_claude_argv() -> Bool { test fn witness_claude_account_default_passes_no_model_flag() -> Bool { let argv = claude_spawn_argv( + effort: ReasoningHigh, model: ProviderAccountDefaultModel, brief: "brief", ) diff --git a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag index ec92e843acc..c6c90766d05 100644 --- a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag @@ -1,6 +1,6 @@ module test.claim.roadmap_provider_events_witness_test -import std.types { Bool, String, Int, List, FilePath } +import std.types { Bool, String, Int, List } import extdeps.languages.json.emit { json_object } import gunbc.harness.harness_wire { WireToolUseBlock } import gunbc.harness.harness_tool_kind { @@ -26,8 +26,6 @@ import gunbc.roadmap_provider_events { parse_codex_jsonl, parse_codex_jsonl_with_line_budget, claude_code_line_codex_kind, - claude_provider_event_projection_args, - codex_provider_event_projection_args, CodexThreadStarted, CodexTurnCompleted, CodexTurnFailed, @@ -488,11 +486,21 @@ test fn claude_stream_json_projects_into_the_codex_event_kinds_the_belt_reads() Present { value: _ } => false } let filter = codex_provider_event_projection_filter as String - system_ok && tool_ok && result_ok && fail_ok && rate_skip + let parsed = parse_codex_jsonl( + text: "{\"type\":\"system\"}\n{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\"}]}}\n{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false}\n", + ) + let inhabitance = match parsed { + CodexEventsRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => false + CodexEventsParsed { events: _ } => + match provider_execution_state(parsed: parsed) { + ProviderCompleted { activity: _ } => true + _ => false + } + } + system_ok && tool_ok && result_ok && fail_ok && rate_skip && inhabitance && string_contains(s: filter, pattern: "$type == \"result\"") && string_contains(s: filter, pattern: "$type == \"system\"") && string_contains(s: filter, pattern: "rate_limit_event") - && claude_provider_event_projection_args(path: "/tmp/e.jsonl" as FilePath) == codex_provider_event_projection_args(path: "/tmp/e.jsonl" as FilePath) } test fn a_harness_tool_round_reaches_the_operator_as_its_own_activity() -> Bool { From e0b697a7fe8164299c9be650e47f28bbd2b9fe1f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 22:34:49 +0000 Subject: [PATCH 3/3] Address review 78389: emit tmux event pipe only for tmux containers. Claude and harness systemd spawn no longer derive readiness from tee/pipe emission or refuse as tmux-event-pipe-emit. Co-authored-by: Cursor --- .../roadmap/roadmap_dispatch_actuator.dag | 158 ++++++++++-------- ...roadmap_dispatch_actuator_witness_test.dag | 28 +++- 2 files changed, 109 insertions(+), 77 deletions(-) diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index 4ea0e5fd1e5..921e9f5611e 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -2793,6 +2793,41 @@ fn dispatch_systemd_unit_container_plan( } } +fn dispatch_tmux_session_container_plan( + instance: HostDashboardInstance, + session_name: String, + events_path: String, + pane_working_directory: String, + worktree_path: String, + provider_inner: List, +) -> DispatchSessionContainerPlan { + match dispatch_attempt_pipe_emission_for_instance( + instance: instance, + events_path: events_path, + ) { + PipeEmitRefused { detail } => + DispatchSessionContainerUnavailable { + step: "tmux-event-pipe-emit" as NonEmptyStr, + detail: detail, + } + PipeEmitted { command } => + DispatchSessionContainerReady { + container: dispatch_tmux_container( + instance: instance, + session_name: session_name, + tmux_spawn: tmux_idle_spawn_argv_for_instance( + instance: instance, + session_name: session_name, + worktree_path: worktree_path, + pane_working_directory: pane_working_directory, + ), + pipe_command: command, + provider_inner: provider_inner, + ), + } + } +} + fn dispatch_session_container_for_provider( provider: DispatchCliProvider, instance: HostDashboardInstance, @@ -2802,8 +2837,7 @@ fn dispatch_session_container_for_provider( events_path: String, worker_log_path: String, pane_working_directory: String, - tmux_spawn: HostExecArgv, - pipe_command: TmuxPipeCommand, + worktree_path: String, provider_inner: List, ) -> DispatchSessionContainerPlan { match provider { @@ -2832,25 +2866,23 @@ fn dispatch_session_container_for_provider( unavailable_why: "headless Claude Code requires a transient unit so stream-json stdout is a unit property and the session placement grant is the cgroup bound", ) CodexDispatchProvider => - DispatchSessionContainerReady { - container: dispatch_tmux_container( - instance: instance, - session_name: session_name, - tmux_spawn: tmux_spawn, - pipe_command: pipe_command, - provider_inner: provider_inner, - ), - } + dispatch_tmux_session_container_plan( + instance: instance, + session_name: session_name, + events_path: events_path, + pane_working_directory: pane_working_directory, + worktree_path: worktree_path, + provider_inner: provider_inner, + ) CursorDispatchProvider => - DispatchSessionContainerReady { - container: dispatch_tmux_container( - instance: instance, - session_name: session_name, - tmux_spawn: tmux_spawn, - pipe_command: pipe_command, - provider_inner: provider_inner, - ), - } + dispatch_tmux_session_container_plan( + instance: instance, + session_name: session_name, + events_path: events_path, + pane_working_directory: pane_working_directory, + worktree_path: worktree_path, + provider_inner: provider_inner, + ) } } @@ -3308,69 +3340,47 @@ fn dispatch_spawn_commands_for_resolved_instance( node_id: node.node, attempt_key: attempt_key, ) - let tmux = tmux_idle_spawn_argv_for_instance( - instance: instance, - session_name: session_name, - worktree_path: worktree_path, - pane_working_directory: dispatch_pane_working_directory( - provider: provider, - instance: instance, - worktree_path: worktree_path, - ), - ) let events_path = dispatch_attempt_events_path_for_instance( instance: instance, node_id: node.node, attempt_key: attempt_key, ) - match dispatch_attempt_pipe_emission_for_instance( + match dispatch_session_container_for_provider( + provider: provider, instance: instance, + node_id: node.node, + attempt_key: attempt_key, + session_name: session_name, events_path: events_path, + worker_log_path: dispatch_attempt_worker_log_path_for_instance( + instance: instance, + node_id: node.node, + attempt_key: attempt_key, + ), + pane_working_directory: dispatch_pane_working_directory( + provider: provider, + instance: instance, + worktree_path: worktree_path, + ), + worktree_path: worktree_path, + provider_inner: provider_inner, ) { - PipeEmitRefused { detail } => - DispatchSpawnRefused { - step: "tmux-event-pipe-emit" as NonEmptyStr, - detail: detail, - } - PipeEmitted { command } => - match dispatch_session_container_for_provider( - provider: provider, - instance: instance, - node_id: node.node, - attempt_key: attempt_key, - session_name: session_name, - events_path: events_path, - worker_log_path: dispatch_attempt_worker_log_path_for_instance( - instance: instance, - node_id: node.node, - attempt_key: attempt_key, - ), - pane_working_directory: dispatch_pane_working_directory( - provider: provider, - instance: instance, - worktree_path: worktree_path, - ), - tmux_spawn: tmux, - pipe_command: command, - provider_inner: provider_inner, - ) { - DispatchSessionContainerUnavailable { step: st, detail: d } => - DispatchSpawnRefused { step: st, detail: d } - DispatchSessionContainerReady { container: c } => - DispatchSpawnReady { - commands: DispatchSpawnCommands { - worktree_add: worktree, - attempt_state_prepare: state_prepare, - attempt_current_pointer: attempt_current_pointer_argv_for_instance( - instance: instance, - node_id: node.node, - attempt_key: attempt_key, - ), - session_container: c, - }, - } + DispatchSessionContainerUnavailable { step: st, detail: d } => + DispatchSpawnRefused { step: st, detail: d } + DispatchSessionContainerReady { container: c } => + DispatchSpawnReady { + commands: DispatchSpawnCommands { + worktree_add: worktree, + attempt_state_prepare: state_prepare, + attempt_current_pointer: attempt_current_pointer_argv_for_instance( + instance: instance, + node_id: node.node, + attempt_key: attempt_key, + ), + session_container: c, + }, } - } + } } } } diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index c66fecbc0d0..3adc71122fa 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -45,7 +45,6 @@ import extdeps.tmux { AttemptPanesParsed, AttemptPanesParseRefused, parse_tmux_attempt_panes_output, - TmuxPipeCommand, } import extdeps.llm.cli { shape_claude_invoke_argv, @@ -1476,8 +1475,7 @@ test fn witness_headless_claude_uses_a_transient_unit_with_stream_json_on_the_ev events_path: events, worker_log_path: log, pane_working_directory: "/wt", - tmux_spawn: HostExecArgv { program: "tmux", args: [] }, - pipe_command: TmuxPipeCommand { body: "true" as NonEmptyStr }, + worktree_path: "/wt", provider_inner: ["claude", "-p"], ) { DispatchSessionContainerUnavailable { step: _, detail: _ } => false @@ -1492,3 +1490,27 @@ test fn witness_headless_claude_uses_a_transient_unit_with_stream_json_on_the_ev } } } + +// review 78389. The spawn fold used to emit the tmux tee/pipe payload before choosing a container, so +// a Claude (systemd stdout) refusal was named tmux-event-pipe-emit. Pipe emission now lives only on +// the tmux container arm; this spawn is the real producer, and a pipe-step refusal here is the defect. +test fn witness_headless_claude_spawn_does_not_refuse_as_tmux_event_pipe() -> Bool { + match dispatch_spawn_commands_for_resolved_instance( + instance: srv1_live_dashboard_instance(), + node: sized_derivable(id: "claude-headless-spawn", prs: [], title: "Headless Claude spawn"), + session_uuid: "00000000-0000-4000-8000-000000000005", + attempt_key: "feedfacefeedface", + provider: ClaudeCodeProvider, + effort: ReasoningMedium, + model: ProviderAccountDefaultModel, + origin: FreshFromBase, + resolution: fixture_ready_resolution(node_id: "claude-headless-spawn", project_ids: []), + ) { + DispatchSpawnRefused { step, detail: _ } => (step as String) != "tmux-event-pipe-emit" + DispatchSpawnReady { commands } => + match commands.session_container { + SystemdUnitContainer { plan: _ } => true + TmuxSessionContainer { spawn: _, default_shell: _, remain_on_exit: _, event_pipe: _, provider_start: _ } => false + } + } +}