From 1d295d4ac253f1e92a42ef80951dadb986522016 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 21:52:28 +0000 Subject: [PATCH 1/6] 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 | 187 ++++++++++++------ 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, 439 insertions(+), 85 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 bc8a43a2662..c0d4651b958 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -94,7 +94,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, @@ -475,10 +476,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 } @@ -812,7 +818,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 => @@ -2353,21 +2377,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, ) } @@ -2503,10 +2521,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, ), ), @@ -2586,10 +2601,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, ), ), @@ -2855,12 +2867,30 @@ fn dispatch_worker_unit_properties( granted: Kibibyte, process_bounds: UnitProcessBounds, exit_hook: NonEmptyStr, +) -> 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 }, @@ -2909,16 +2939,18 @@ type DispatchWorkerUnitPlan { systemd_run_program: FilePath unit: NonEmptyStr working_directory: String - worker_log_path: String + stdout_path: String + stderr_path: String inner_argv: List exit_hook: NonEmptyStr } 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, exit_hook: plan.exit_hook, @@ -2947,6 +2979,49 @@ 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 } => + match attempt_exit_hook(instance_root: instance.instance_root as String, node_id: node_id, attempt_key: attempt_key, role: AttemptWorkerRole) { + AttemptExitHookRefused { reason } => + DispatchSessionContainerUnavailable { step: "worker-exit-hook" as NonEmptyStr, detail: reason as String } + AttemptExitHookRendered { exec_line } => + 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, + exit_hook: exec_line, + }, + }, + } + } + } +} + fn dispatch_session_container_for_provider( provider: DispatchCliProvider, instance: HostDashboardInstance, @@ -2962,45 +3037,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 } => - match attempt_exit_hook(instance_root: instance.instance_root as String, node_id: node_id, attempt_key: attempt_key, role: AttemptWorkerRole) { - AttemptExitHookRefused { reason } => - DispatchSessionContainerUnavailable { step: "worker-exit-hook" as NonEmptyStr, detail: reason as String } - AttemptExitHookRendered { exec_line } => - 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, - exit_hook: exec_line, - }, - }, - } - } - } + 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 e7b780b0886..5a59f9201ba 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, @@ -169,6 +172,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 } } @@ -184,6 +190,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 } } @@ -199,6 +208,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 } } @@ -219,6 +231,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 32867dd4f52..4c0e841d7f6 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -46,6 +46,7 @@ import extdeps.tmux { AttemptPanesParsed, AttemptPanesParseRefused, parse_tmux_attempt_panes_output, + TmuxPipeCommand, } import extdeps.llm.cli { shape_claude_invoke_argv, @@ -64,6 +65,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, @@ -110,6 +114,9 @@ import gunbc.roadmap_dispatch_actuator { HostExecArgv, SystemdUnitContainer, TmuxSessionContainer, + dispatch_session_container_for_provider, + DispatchSessionContainerReady, + DispatchSessionContainerUnavailable, DispatchSpawnReady, DispatchSpawnRefused, git_worktree_add_argv, @@ -302,15 +309,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") } @@ -326,10 +331,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( @@ -923,10 +925,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") @@ -1072,10 +1071,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( @@ -1086,10 +1082,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") @@ -1518,3 +1511,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 b364c7948df3dfe1400daa562fd1206f6fb0aba4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 22:17:05 +0000 Subject: [PATCH 2/6] 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 c0d4651b958..71e5b566d4b 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -476,15 +476,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 } @@ -818,25 +814,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 => @@ -2377,6 +2355,7 @@ fn git_worktree_add_detached_argv_for_instance( } fn claude_spawn_argv( + effort: ReasoningEffort, model: ProviderModelSelection, brief: String, ) -> List { @@ -2385,6 +2364,7 @@ fn claude_spawn_argv( start_prompt: dispatch_claude_start_prompt, permission_args: dispatch_claude_permission_args, model: model, + effort: effort, turn_bound: ClaudePrintUnboundedTurns, ) } @@ -2521,6 +2501,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, ), @@ -2601,6 +2582,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 4c0e841d7f6..0fe9f8e5662 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -309,12 +309,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") } @@ -331,6 +333,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", ) @@ -925,6 +928,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", ) @@ -1071,6 +1075,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", ) @@ -1082,6 +1087,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 86263c42f80d03c552c39ce9241a6f2298cb0de0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 22:34:49 +0000 Subject: [PATCH 3/6] 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 71e5b566d4b..977ccc2fcff 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -3004,6 +3004,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, @@ -3013,8 +3048,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 { @@ -3043,25 +3077,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, + ) } } @@ -3529,69 +3561,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 0fe9f8e5662..befdb13f74c 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -46,7 +46,6 @@ import extdeps.tmux { AttemptPanesParsed, AttemptPanesParseRefused, parse_tmux_attempt_panes_output, - TmuxPipeCommand, } import extdeps.llm.cli { shape_claude_invoke_argv, @@ -1541,8 +1540,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 @@ -1557,3 +1555,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 + } + } +} From dc735fe6993e91393069e6a61b3247f91879b39b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 10 Oct 2026 04:10:21 +0000 Subject: [PATCH 4/6] Replace CodexProviderEventKind with a provider-neutral classifier. Delete the Codex-named event vocabulary at the root. One typed classifier decides observe, skip, or refuse; jq only bounds and preserves fields. Co-authored-by: Cursor --- dag/gunbc/roadmap/roadmap_provider_events.dag | 256 ++++++++---------- .../roadmap_provider_events_witness_test.dag | 159 +++++++---- 2 files changed, 215 insertions(+), 200 deletions(-) diff --git a/dag/gunbc/roadmap/roadmap_provider_events.dag b/dag/gunbc/roadmap/roadmap_provider_events.dag index 0bd82f6da62..6491af2678d 100644 --- a/dag/gunbc/roadmap/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap/roadmap_provider_events.dag @@ -59,7 +59,10 @@ type CodexItemActivity | HarnessRefusingUnrecognizedTool | CodexUnclassifiedItem -// THE GUNBC HARNESS INTERLEAVES EVENTS OF ITS OWN in the Codex-shaped stream: round.usage, the +// ONE SHARED EVENT VOCABULARY. Codex, Claude Code, and the gunbc harness write one JSONL +// stream the belt reads; the kind is not a Codex type that other providers nickname into. +// CodexProviderEventKind is deleted (DESIGN §3 replacement migration at the root). +// THE GUNBC HARNESS INTERLEAVES EVENTS OF ITS OWN in that stream: round.usage, the // per-round token and clock accounting the dashboard's round strip reads (gunbc.harness.harness_throughput); // turn.alignment_checkpoint_due, the epoch-boundary announcement a chain-carrying turn ends on; // and turn.wire_log_unwritable, the non-terminal notice that a round's durable wire-log write @@ -68,37 +71,48 @@ type CodexItemActivity // serving seat before any provider call (gunbc.harness.harness_cli harness_bind_seat_waiting). Before these arms the whole stream was refused at such a line as an unknown // event type, so a live harness attempt rendered as "Agent evidence refused" with no strip // (first belt-dispatched attempt, 2026-09-04). -type CodexProviderEventKind - = CodexThreadStarted - | CodexTurnStarted - | CodexItemStarted { activity: CodexItemActivity } - | CodexItemCompleted { activity: CodexItemActivity } - | CodexTurnCompleted - | CodexTurnFailed - | CodexError +type ProviderEventKind + = ThreadStarted + | TurnStarted + | ItemStarted { activity: CodexItemActivity } + | ItemCompleted { activity: CodexItemActivity } + | TurnCompleted + | TurnFailed + | ProviderStreamError | HarnessRoundUsage | HarnessBudgetExhausted | HarnessAlignmentCheckpointDue | HarnessWireLogUnwritable | HarnessPlacementWaiting -type CodexProviderEvent { +// Skip is not refuse: a line the classifier names as noise is dropped and the stream continues. +// Refuse is a located stream failure. The classifier decides which; jq does not. +type ProviderEventSkipReason + = SkipRateLimitEvent + | SkipClaudeUserWithoutToolResult + +type ProviderEventLineDisposition + = ProviderEventObserved { kind: ProviderEventKind } + | ProviderEventSkipped { reason: ProviderEventSkipReason } + | ProviderEventLineRefused { reason: String } + +type ProviderEvent { line_number: Int raw: String - kind: CodexProviderEventKind + kind: ProviderEventKind } -type CodexEventParse - = CodexEventsParsed { events: List } - | CodexEventsRefused { - events_before_refusal: List, +type ProviderEventParse + = ProviderEventsParsed { events: List } + | ProviderEventStreamRefused { + events_before_refusal: List, line_number: Int, raw: String, reason: String, } -type CodexParseFold { - events: List +type ProviderParseFold { + events: List next_line_number: Int total_lines: Int refused: Bool @@ -175,7 +189,7 @@ data codex_provider_event_projection_filter: NonEmptyStr = join([ 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", + "] else \"unknown\" end)}] else [] end)}} else {\"type\":$type} end", ], "") as NonEmptyStr fn codex_provider_event_projection_args(path: FilePath) -> List { @@ -187,30 +201,34 @@ fn codex_provider_event_projection_args(path: FilePath) -> List { ] } -fn claude_code_line_codex_kind(line: ClaudeCodeStreamLine) -> CodexProviderEventKind? { +fn classify_claude_code_stream_line(line: ClaudeCodeStreamLine) -> ProviderEventLineDisposition { match line { - ClaudeCodeSystemLine => Present { value: CodexThreadStarted } + ClaudeCodeSystemLine => + ProviderEventObserved { kind: ThreadStarted } ClaudeCodeAssistantLine { has_tool_use } => if has_tool_use { - Present { value: CodexItemStarted { activity: CodexRunningCommand } } + ProviderEventObserved { kind: ItemStarted { activity: CodexRunningCommand } } } else { - Present { value: CodexItemCompleted { activity: CodexReporting } } + ProviderEventObserved { kind: ItemCompleted { activity: CodexReporting } } } ClaudeCodeUserLine { has_tool_result } => if has_tool_result { - Present { value: CodexItemCompleted { activity: CodexRunningCommand } } + ProviderEventObserved { kind: ItemCompleted { activity: CodexRunningCommand } } } else { - none + ProviderEventSkipped { reason: SkipClaudeUserWithoutToolResult } } ClaudeCodeResultLine { result } => if result.is_error { - Present { value: CodexTurnFailed } + ProviderEventObserved { kind: TurnFailed } } else { - Present { value: CodexTurnCompleted } + ProviderEventObserved { kind: TurnCompleted } } - ClaudeCodeRateLimitEventLine { info: _ } => none - ClaudeCodeOtherLine { event_type: _ } => none - ClaudeCodeLineUnreadable { gap: _ } => none + ClaudeCodeRateLimitEventLine { info: _ } => + ProviderEventSkipped { reason: SkipRateLimitEvent } + ClaudeCodeOtherLine { event_type: _ } => + ProviderEventLineRefused { reason: "unknown provider JSONL event type" } + ClaudeCodeLineUnreadable { gap: _ } => + ProviderEventLineRefused { reason: "malformed Claude stream-json line" } } } @@ -225,7 +243,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 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." +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 classify_provider_event_line is the one typed classifier: jq bounds and preserves fields and makes no skip-or-refuse verdict." // 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, @@ -432,102 +450,43 @@ fn codex_item_activity_label(activity: CodexItemActivity) -> String { } } -fn codex_event_from_complete_line( - raw: String, - line_number: Int, -) -> CodexProviderEvent? { +fn classify_provider_event_line(raw: String) -> ProviderEventLineDisposition { if codex_line_has_top_level_type(raw: raw, event_type: "thread.started") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexThreadStarted, - } } + ProviderEventObserved { kind: ThreadStarted } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.started") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexTurnStarted, - } } + ProviderEventObserved { kind: TurnStarted } } else if codex_line_has_top_level_type(raw: raw, event_type: "item.started") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexItemStarted { activity: codex_item_activity(raw: raw) }, - } } + ProviderEventObserved { kind: ItemStarted { activity: codex_item_activity(raw: raw) } } } else if codex_line_has_top_level_type(raw: raw, event_type: "item.completed") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexItemCompleted { activity: codex_item_activity(raw: raw) }, - } } + ProviderEventObserved { kind: ItemCompleted { activity: codex_item_activity(raw: raw) } } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.completed") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexTurnCompleted, - } } + ProviderEventObserved { kind: TurnCompleted } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.failed") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexTurnFailed, - } } + ProviderEventObserved { kind: TurnFailed } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.budget_exhausted") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: HarnessBudgetExhausted, - } } + ProviderEventObserved { kind: HarnessBudgetExhausted } } else if codex_line_has_top_level_type(raw: raw, event_type: "round.usage") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: HarnessRoundUsage, - } } + ProviderEventObserved { kind: HarnessRoundUsage } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.alignment_checkpoint_due") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: HarnessAlignmentCheckpointDue, - } } + ProviderEventObserved { kind: HarnessAlignmentCheckpointDue } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.placement_waiting") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: HarnessPlacementWaiting, - } } + ProviderEventObserved { kind: HarnessPlacementWaiting } } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.wire_log_unwritable") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: HarnessWireLogUnwritable, - } } + ProviderEventObserved { kind: HarnessWireLogUnwritable } } else if codex_line_has_top_level_type(raw: raw, event_type: "error") { - Present { value: CodexProviderEvent { - line_number: line_number, - raw: raw, - kind: CodexError, - } } + ProviderEventObserved { kind: ProviderStreamError } } else { - 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 - } + classify_claude_code_stream_line(line: claude_code_stream_line(line: raw)) } } -fn codex_parse_fold_line( - state: CodexParseFold, +fn provider_parse_fold_line( + state: ProviderParseFold, line: String, line_budget: CharacterCount, -) -> CodexParseFold { +) -> ProviderParseFold { if state.refused { - CodexParseFold { + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, @@ -542,7 +501,7 @@ fn codex_parse_fold_line( let is_trailing_empty = raw == "" && state.next_line_number == state.total_lines if is_trailing_empty { - CodexParseFold { + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, @@ -552,51 +511,62 @@ fn codex_parse_fold_line( refused_reason: "", } } else if !measure_le(a: raw_chars, b: line_budget) { - CodexParseFold { + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, refused: true, refused_line_number: state.next_line_number, refused_raw: join([ - "", ], ""), refused_reason: join([ - "Codex JSONL event exceeds observation line budget ", + "provider JSONL event exceeds observation line budget ", to_string(character_count_value(c: line_budget)), " chars; full raw remains in attempt evidence", ], ""), } } else if raw == "" { - CodexParseFold { + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, refused: true, refused_line_number: state.next_line_number, refused_raw: line, - refused_reason: "empty line inside Codex JSONL stream", + refused_reason: "empty line inside provider JSONL stream", } } else if !json_text_parseable(s: raw) { - CodexParseFold { + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, refused: true, refused_line_number: state.next_line_number, refused_raw: line, - refused_reason: "malformed or truncated Codex JSONL object", + refused_reason: "malformed or truncated provider JSONL object", } } else { - match codex_event_from_complete_line( - raw: raw, - line_number: state.next_line_number, - ) { - Present { value: event } => - CodexParseFold { - events: append(state.events, [event]), + match classify_provider_event_line(raw: raw) { + ProviderEventObserved { kind } => + ProviderParseFold { + events: append(state.events, [ProviderEvent { + line_number: state.next_line_number, + raw: raw, + kind: kind, + }]), + next_line_number: state.next_line_number + 1, + total_lines: state.total_lines, + refused: false, + refused_line_number: 0, + refused_raw: "", + refused_reason: "", + } + ProviderEventSkipped { reason: _ } => + ProviderParseFold { + events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, refused: false, @@ -604,15 +574,15 @@ fn codex_parse_fold_line( refused_raw: "", refused_reason: "", } - Absent => - CodexParseFold { + ProviderEventLineRefused { reason } => + ProviderParseFold { events: state.events, next_line_number: state.next_line_number + 1, total_lines: state.total_lines, refused: true, refused_line_number: state.next_line_number, refused_raw: line, - refused_reason: "unknown Codex JSONL event type", + refused_reason: reason, } } } @@ -622,14 +592,14 @@ fn codex_parse_fold_line( fn parse_codex_jsonl_with_line_budget( text: String, line_budget: CharacterCount, -) -> CodexEventParse { +) -> ProviderEventParse { if text == "" { - CodexEventsParsed { events: [] } + ProviderEventsParsed { events: [] } } else { let lines = split(s: text, delimiter: "\n") let state = fold( lines, - init: CodexParseFold { + init: ProviderParseFold { events: [], next_line_number: 1, total_lines: count(lines), @@ -639,7 +609,7 @@ fn parse_codex_jsonl_with_line_budget( refused_reason: "", }, f: fn(acc, line) { - codex_parse_fold_line( + provider_parse_fold_line( state: acc, line: line, line_budget: line_budget, @@ -647,19 +617,19 @@ fn parse_codex_jsonl_with_line_budget( }, ) if state.refused { - CodexEventsRefused { + ProviderEventStreamRefused { events_before_refusal: state.events, line_number: state.refused_line_number, raw: state.refused_raw, reason: state.refused_reason, } } else { - CodexEventsParsed { events: state.events } + ProviderEventsParsed { events: state.events } } } } -fn parse_codex_jsonl(text: String) -> CodexEventParse { +fn parse_codex_jsonl(text: String) -> ProviderEventParse { parse_codex_jsonl_with_line_budget( text: text, line_budget: codex_provider_event_line_observation_budget, @@ -694,7 +664,7 @@ fn harness_placement_waiting_detail(raw: String) -> String { fn provider_execution_fold_event( state: ProviderExecutionState, - event: CodexProviderEvent, + event: ProviderEvent, ) -> ProviderExecutionState { match state { ProviderCompleted { activity: _ } => state @@ -703,8 +673,8 @@ fn provider_execution_fold_event( ProviderEventsRefused { activity: _, detail: _ } => state _ => match event.kind { - CodexThreadStarted => ProviderStarting { activity: "thread started" } - CodexTurnStarted => ProviderRunning { activity: "agent started" } + ThreadStarted => ProviderStarting { activity: "thread started" } + TurnStarted => ProviderRunning { activity: "agent started" } HarnessRoundUsage => state HarnessWireLogUnwritable => state HarnessPlacementWaiting => ProviderAwaitingSeat { detail: harness_placement_waiting_detail(raw: event.raw) } @@ -713,14 +683,14 @@ fn provider_execution_fold_event( activity: "Alignment checkpoint due · adjudication decides resume", detail: event.raw, } - CodexItemStarted { activity } => + ItemStarted { activity } => ProviderRunning { activity: join( ["Agent working · ", codex_item_activity_label(activity: activity)], "", ), } - CodexItemCompleted { activity } => + ItemCompleted { activity } => ProviderRunning { activity: match activity { CodexReporting => "Agent finishing" @@ -731,21 +701,21 @@ fn provider_execution_fold_event( ), }, } - CodexTurnCompleted => + TurnCompleted => ProviderCompleted { activity: "Agent complete" } - CodexTurnFailed => + TurnFailed => ProviderFailed { activity: "agent turn failed", detail: event.raw } HarnessBudgetExhausted => ProviderBudgetSpent { activity: "Agent stopped at its round budget", detail: event.raw } - CodexError => + ProviderStreamError => ProviderFailed { activity: "provider error", detail: event.raw } } } } -fn provider_execution_state(parsed: CodexEventParse) -> ProviderExecutionState { +fn provider_execution_state(parsed: ProviderEventParse) -> ProviderExecutionState { match parsed { - CodexEventsParsed { events } => + ProviderEventsParsed { events } => fold( events, init: ProviderNotStarted, @@ -753,7 +723,7 @@ fn provider_execution_state(parsed: CodexEventParse) -> ProviderExecutionState { provider_execution_fold_event(state: state, event: event) }, ) - CodexEventsRefused { + ProviderEventStreamRefused { events_before_refusal, line_number, raw, 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 c6c90766d05..5f8bf58a0a4 100644 --- a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag @@ -8,15 +8,13 @@ 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, HarnessEditingFile, HarnessRefusingUnrecognizedTool, CodexUnclassifiedItem, - CodexItemStarted, CodexItemCompleted, - CodexEventsParsed, - CodexEventsRefused, + ItemStarted, ItemCompleted, + ProviderEventsParsed, + ProviderEventStreamRefused, ProviderRunning, ProviderAwaitingSeat, ProviderCompleted, @@ -25,12 +23,15 @@ import gunbc.roadmap_provider_events { ProviderEventsRefused, parse_codex_jsonl, parse_codex_jsonl_with_line_budget, - claude_code_line_codex_kind, - CodexThreadStarted, - CodexTurnCompleted, - CodexTurnFailed, - CodexItemStarted, - CodexRunningCommand, + classify_provider_event_line, + ProviderEventObserved, + ProviderEventSkipped, + ProviderEventLineRefused, + SkipRateLimitEvent, + SkipClaudeUserWithoutToolResult, + ThreadStarted, + TurnCompleted, + TurnFailed, codex_provider_event_line_observation_budget, codex_provider_projection_type_budget, codex_provider_projection_detail_budget, @@ -48,7 +49,7 @@ test fn a_harness_round_usage_event_is_accounted_not_refused() -> Bool { text: "{\"type\":\"turn.started\"}\n{\"type\":\"round.usage\",\"step\":1,\"input_tokens\":900,\"output_tokens\":120,\"epoch_ms\":1000000,\"files_changed\":0}\n{\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\"}}\n", ) match parsed { - CodexEventsRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => false + ProviderEventStreamRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => false _ => match provider_execution_state(parsed: parsed) { ProviderRunning { activity } => activity == "Agent working · running command" @@ -128,8 +129,8 @@ test fn codex_error_is_not_absorbed_into_spawned() -> Bool { test fn truncated_line_refuses_with_location_and_raw() -> Bool { let truncated = "{\"type\":\"thread.started\",\"thread_id\":\"thread-1\"}\n{\"type\":\"item.started\"" match parse_codex_jsonl(text: truncated) { - CodexEventsParsed { events: _ } => false - CodexEventsRefused { + ProviderEventsParsed { events: _ } => false + ProviderEventStreamRefused { events_before_refusal, line_number, raw, @@ -146,7 +147,7 @@ test fn unknown_event_refuses_instead_of_guessing_progress() -> Bool { let parsed = parse_codex_jsonl(text: "{\"type\":\"future.event\",\"value\":1}\n") match provider_execution_state(parsed: parsed) { ProviderEventsRefused { activity: _, detail } => - string_contains(s: detail, pattern: "unknown Codex JSONL event type") + string_contains(s: detail, pattern: "unknown provider JSONL event type") && string_contains(s: detail, pattern: "future.event") _ => false } @@ -158,15 +159,15 @@ test fn nested_type_token_cannot_fabricate_terminal_success() -> Bool { ) match provider_execution_state(parsed: parsed) { ProviderEventsRefused { activity: _, detail } => - string_contains(s: detail, pattern: "unknown Codex JSONL event type") + string_contains(s: detail, pattern: "malformed Claude stream-json line") _ => false } } test fn braced_but_invalid_json_cannot_fabricate_terminal_success() -> Bool { match parse_codex_jsonl(text: "{\"type\":\"turn.completed\",}\n") { - CodexEventsParsed { events: _ } => false - CodexEventsRefused { + ProviderEventsParsed { events: _ } => false + ProviderEventStreamRefused { events_before_refusal, line_number, raw, @@ -190,8 +191,8 @@ test fn oversized_event_refuses_without_echoing_payload() -> Bool { text: oversized, line_budget: character_count(count: 48), ) { - CodexEventsParsed { events: _ } => false - CodexEventsRefused { + ProviderEventsParsed { events: _ } => false + ProviderEventStreamRefused { events_before_refusal, line_number, raw, @@ -399,16 +400,16 @@ fn tool_use_round_tags(name: String) -> String { uses: [WireToolUseBlock { id: "toolu_1", name: name, input: json_object([]) }], ) match parse_codex_jsonl(text: concat(join(lines, "\n"), "\n")) { - CodexEventsRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => "refused" - CodexEventsParsed { events } => + ProviderEventStreamRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => "refused" + ProviderEventsParsed { events } => join( fold( events, init: [], f: (acc, e) => concat(acc, [ match e.kind { - CodexItemStarted { activity } => join(["started:", activity_tag(activity: activity)], "") - CodexItemCompleted { activity } => join(["completed:", activity_tag(activity: activity)], "") + ItemStarted { activity } => join(["started:", activity_tag(activity: activity)], "") + ItemCompleted { activity } => join(["completed:", activity_tag(activity: activity)], "") _ => "not-an-item" }, ]), @@ -444,63 +445,107 @@ 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 } => +test fn classify_claude_system_line_is_thread_started() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"system\",\"subtype\":\"init\"}") { + ProviderEventObserved { kind } => match kind { - CodexThreadStarted => true + ThreadStarted => true _ => false } - Absent => false + _ => 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 } => +} + +test fn classify_claude_assistant_tool_use_is_item_started_running() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\",\"name\":\"Bash\"}]}}") { + ProviderEventObserved { kind } => match kind { - CodexItemStarted { activity } => + ItemStarted { activity } => match activity { CodexRunningCommand => true _ => false } _ => false } - Absent => false + _ => 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 } => +} + +test fn classify_claude_result_success_is_turn_completed() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false}") { + ProviderEventObserved { kind } => match kind { - CodexTurnCompleted => true + TurnCompleted => true _ => false } - Absent => false + _ => 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 } => +} + +test fn classify_claude_result_error_is_turn_failed() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":true,\"api_error_status\":401}") { + ProviderEventObserved { kind } => match kind { - CodexTurnFailed => true + TurnFailed => true _ => false } - Absent => false + _ => 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 +} + +test fn classify_claude_rate_limit_is_skipped() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"rate_limit_event\",\"rate_limit_info\":{\"status\":\"allowed\",\"unifiedWindows\":{}}}") { + ProviderEventSkipped { reason } => + match reason { + SkipRateLimitEvent => true + _ => false + } + _ => false } - let filter = codex_provider_event_projection_filter as String - 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 +} + +test fn classify_claude_user_without_tool_result_is_skipped() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"user\",\"message\":{\"content\":[{\"type\":\"text\"}]}}") { + ProviderEventSkipped { reason } => + match reason { + SkipClaudeUserWithoutToolResult => true _ => false } + _ => false + } +} + +test fn classify_unknown_event_type_is_refused() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"future.event\",\"value\":1}") { + ProviderEventLineRefused { reason } => + string_contains(s: reason, pattern: "unknown provider JSONL event type") + _ => false + } +} + +test fn classify_malformed_claude_result_line_is_refused() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"result\"}") { + ProviderEventLineRefused { reason } => + string_contains(s: reason, pattern: "malformed Claude stream-json line") + _ => false + } +} + +// INHABITANCE: the real parse fold over a captured stream shape, including a skipped rate-limit +// line between observations. Deleting parse_codex_jsonl would make this fail. +test fn a_claude_stream_json_turn_is_read_as_provider_events_including_skipped_rate_limit() -> Bool { + let parsed = parse_codex_jsonl( + text: "{\"type\":\"system\"}\n{\"type\":\"rate_limit_event\",\"rate_limit_info\":{\"status\":\"allowed\",\"unifiedWindows\":{}}}\n{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\"}]}}\n{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false}\n", + ) + match parsed { + ProviderEventStreamRefused { events_before_refusal: _, line_number: _, raw: _, reason: _ } => false + ProviderEventsParsed { events } => + count(events) == 3 + && 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") } test fn a_harness_tool_round_reaches_the_operator_as_its_own_activity() -> Bool { From 01bc44f15b16b398b2ae2fe0edcb528c6f1cbcf6 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 10 Oct 2026 04:20:30 +0000 Subject: [PATCH 5/6] Refuse fabricated Claude projector fields and malformed tool-use content. jq no longer fills is_error or subtype; the typed reader refuses a missing or non-boolean is_error. Assistant and user content that is not a well-formed block list is unreadable rather than treated as no tool use. Co-authored-by: Cursor --- dag/extdeps/llm/claude_code_stream_json.dag | 85 +++++++++++++------ dag/gunbc/roadmap/roadmap_provider_events.dag | 6 +- .../roadmap_provider_events_witness_test.dag | 30 +++++++ 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/dag/extdeps/llm/claude_code_stream_json.dag b/dag/extdeps/llm/claude_code_stream_json.dag index a05928e1a00..af76abc5d38 100644 --- a/dag/extdeps/llm/claude_code_stream_json.dag +++ b/dag/extdeps/llm/claude_code_stream_json.dag @@ -433,37 +433,70 @@ fn claude_result_line(doc: JsonValue) -> ClaudeCodeStreamLine { } } -fn claude_content_blocks_have_type(content: JsonValue, block_type: String) -> Bool { +type ClaudeContentTypeProbe + = ContentBlocksHaveType + | ContentBlocksLackType + | ContentBlocksUnreadable { gap: ClaudeCodeStreamLineGap } + +fn claude_content_blocks_have_type(content: JsonValue, block_type: String) -> ClaudeContentTypeProbe { 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 - } + fold( + elements, + init: ContentBlocksLackType, + f: fn(acc, e) { + match acc { + ContentBlocksUnreadable { gap } => ContentBlocksUnreadable { gap: gap } + ContentBlocksHaveType => + match claude_string_member(v: e, key: "type") { + StringMemberRead { value: _ } => ContentBlocksHaveType + StringMemberAbsent => ContentBlocksHaveType + StringMemberNull => + ContentBlocksUnreadable { gap: StreamLineMemberMissing { member: "message.content[].type" } } + StringMemberMalformed => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content[].type" } } + } + ContentBlocksLackType => + match claude_string_member(v: e, key: "type") { + StringMemberRead { value: s } => + if s == block_type { + ContentBlocksHaveType + } else { + ContentBlocksLackType + } + StringMemberAbsent => ContentBlocksLackType + StringMemberNull => + ContentBlocksUnreadable { gap: StreamLineMemberMissing { member: "message.content[].type" } } + StringMemberMalformed => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content[].type" } } + } + } + }, ) - JsonNull => false - JsonBool { value: _ } => false - JsonNumber { lexeme: _ } => false - JsonString { value: _ } => false - JsonObject { members: _ } => false + JsonNull => ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } + JsonBool { value: _ } => ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } + JsonNumber { lexeme: _ } => ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } + JsonString { value: _ } => ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } + JsonObject { members: _ } => ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } } } -fn claude_message_content_has_type(doc: JsonValue, block_type: String) -> Bool { +fn claude_message_content_has_type(doc: JsonValue, block_type: String) -> ClaudeContentTypeProbe { 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 => ContentBlocksLackType + JsonMemberDuplicated { count: _ } => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message.content" } } + JsonMemberNotAnObject => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message" } } } - JsonMemberAbsent => false - JsonMemberDuplicated { count: _ } => false - JsonMemberNotAnObject => false + JsonMemberAbsent => ContentBlocksLackType + JsonMemberDuplicated { count: _ } => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message" } } + JsonMemberNotAnObject => + ContentBlocksUnreadable { gap: StreamLineMemberMalformed { member: "message" } } } } @@ -488,13 +521,17 @@ fn claude_code_stream_line(line: String) -> ClaudeCodeStreamLine { 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), + match claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_use) { + ContentBlocksHaveType => ClaudeCodeAssistantLine { has_tool_use: true } + ContentBlocksLackType => ClaudeCodeAssistantLine { has_tool_use: false } + ContentBlocksUnreadable { gap } => ClaudeCodeLineUnreadable { gap: gap } } } 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), + match claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_result) { + ContentBlocksHaveType => ClaudeCodeUserLine { has_tool_result: true } + ContentBlocksLackType => ClaudeCodeUserLine { has_tool_result: false } + ContentBlocksUnreadable { gap } => ClaudeCodeLineUnreadable { gap: gap } } } else { ClaudeCodeOtherLine { event_type: t } diff --git a/dag/gunbc/roadmap/roadmap_provider_events.dag b/dag/gunbc/roadmap/roadmap_provider_events.dag index 6491af2678d..3dcee34df6c 100644 --- a/dag/gunbc/roadmap/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap/roadmap_provider_events.dag @@ -185,11 +185,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 {\"type\":$type,\"subtype\":(if (.subtype | type) == \"string\" then .subtype[0:", + ")} 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} + (if (.subtype | type) == \"string\" then {\"subtype\":.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:", + "]} else {} end) + (if (.is_error | type) == \"boolean\" then {\"is_error\":.is_error} else {} end)) elif $type == \"system\" then {\"type\":$type} elif $type == \"assistant\" or $type == \"user\" then {\"type\":$type,\"message\":(if (.message | type) == \"object\" then {\"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)}} else {\"type\":$type} end", + "] else .type end)}] else .message.content end)} else .message end)} else {\"type\":$type} end", ], "") as NonEmptyStr fn codex_provider_event_projection_args(path: FilePath) -> List { 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 5f8bf58a0a4..ea56fe9ec54 100644 --- a/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_provider_events_witness_test.dag @@ -531,6 +531,36 @@ test fn classify_malformed_claude_result_line_is_refused() -> Bool { } } +test fn classify_result_with_non_boolean_is_error_is_refused() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":\"yes\"}") { + ProviderEventLineRefused { reason } => + string_contains(s: reason, pattern: "malformed Claude stream-json line") + _ => false + } +} + +test fn classify_malformed_assistant_block_type_is_refused() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":1}]}}") { + ProviderEventLineRefused { reason } => + string_contains(s: reason, pattern: "malformed Claude stream-json line") + _ => false + } +} + +test fn classify_non_array_assistant_content_is_refused() -> Bool { + match classify_provider_event_line(raw: "{\"type\":\"assistant\",\"message\":{\"content\":\"tool_use\"}}") { + ProviderEventLineRefused { reason } => + string_contains(s: reason, pattern: "malformed Claude stream-json line") + _ => false + } +} + +test fn jq_result_arm_does_not_fabricate_is_error_or_subtype() -> Bool { + let filter = codex_provider_event_projection_filter as String + !string_contains(s: filter, pattern: "else true end") + && string_contains(s: filter, pattern: "then {\"is_error\":.is_error} else {} end") +} + // INHABITANCE: the real parse fold over a captured stream shape, including a skipped rate-limit // line between observations. Deleting parse_codex_jsonl would make this fail. test fn a_claude_stream_json_turn_is_read_as_provider_events_including_skipped_rate_limit() -> Bool { From 142a0232327270cbd3ce889b819fdce6b7ddca00 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 10 Oct 2026 15:59:17 +0000 Subject: [PATCH 6/6] Thread closeout ExecStopPost through the split stdout/stderr unit properties. Rebase onto #13641 left stream_properties using exit_hook without declaring it, so the headless spawn claims could not resolve. Co-authored-by: Cursor --- dag/gunbc/roadmap/roadmap_dispatch_actuator.dag | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index 977ccc2fcff..2e7d9601f26 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -2857,6 +2857,7 @@ fn dispatch_worker_unit_properties( stderr_path: worker_log_path, granted: granted, process_bounds: process_bounds, + exit_hook: exit_hook, ) } @@ -2867,6 +2868,7 @@ fn dispatch_worker_unit_stream_properties( stderr_path: String, granted: Kibibyte, process_bounds: UnitProcessBounds, + exit_hook: NonEmptyStr, ) -> List { concat( compute_grant_unit_properties(granted: granted, working_directory: working_directory, process_bounds: Present { value: process_bounds }),