From 53cde074aacdbbd25fc11218bc46ef69d134ed16 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 00:20:52 +0000 Subject: [PATCH 01/10] WIP: Direct-process observation substrate --- dag/gunbc/direct_process_observation.dag | 52 ++++++++++++ dag/gunbc/roadmap_belt_actuate.dag | 82 ++++--------------- dag/gunbc/roadmap_dashboard_instance.dag | 8 -- dag/gunbc/roadmap_dispatch_actuator.dag | 22 ----- dag/gunbc/roadmap_execution_contract.dag | 3 - dag/gunbc/roadmap_provider_events.dag | 35 +------- dag/gunbc/roadmap_serve.dag | 2 +- dag/gunbc/roadmap_workflow_progress.dag | 2 +- ...irect_process_observation_witness_test.dag | 37 +++++++++ ...dmap_dispatch_environment_witness_test.dag | 2 - 10 files changed, 110 insertions(+), 135 deletions(-) create mode 100644 dag/gunbc/direct_process_observation.dag create mode 100644 dag/test/claim/direct_process_observation_witness_test.dag diff --git a/dag/gunbc/direct_process_observation.dag b/dag/gunbc/direct_process_observation.dag new file mode 100644 index 00000000000..ecf8bd34d6a --- /dev/null +++ b/dag/gunbc/direct_process_observation.dag @@ -0,0 +1,52 @@ +module gunbc.direct_process_observation + +import std.types { String, NonEmptyStr } + +type DirectProviderEventsObservation + = DirectProviderEventsObserved { content: String } + | DirectProviderEventsRefused { reason: NonEmptyStr } + +data direct_process_observation_note: String = "The substrate authority for reading attempt-owned provider evidence directly, without shelling out to a foreign projector. Complete Codex JSONL remains in provider-events.jsonl; observation reads that file through Filesystem.Read and hands the bytes to gunbc.roadmap_provider_events.parse_codex_jsonl, which enforces the CharacterCount line budget and anchored envelope classification in-process. This dissolves codex_provider_event_projection_filter and ProviderEventProjectionCapability (review 44058 scaffold; codex_projection_filter_scaffold_note) — the jq program existed only to bound a line before the substrate could read it, which is the purity trap DESIGN section 6 names once parse_codex_jsonl_with_line_budget landed. A read refusal is typed and located; it is never conflated with an empty event stream." + +fn direct_provider_events_observation_readable( + observation: DirectProviderEventsObservation, +) -> Bool { + match observation { + DirectProviderEventsObserved { content: _ } => true + DirectProviderEventsRefused { reason: _ } => false + } +} + +fn direct_provider_events_observation_content( + observation: DirectProviderEventsObservation, +) -> String { + match observation { + DirectProviderEventsObserved { content } => content + DirectProviderEventsRefused { reason: _ } => "" + } +} + +fn direct_provider_events_observation_error( + observation: DirectProviderEventsObservation, +) -> String { + match observation { + DirectProviderEventsObserved { content: _ } => "" + DirectProviderEventsRefused { reason } => reason as String + } +} + +fn observe_provider_events_file(path: String) -> DirectProviderEventsObservation { + let read = Filesystem.Read(path: path) + if read.success { + DirectProviderEventsObserved { content: read.content } + } else { + DirectProviderEventsRefused { + reason: join([ + "provider event stream is unreadable at ", + path, + ": ", + read.error, + ], "") as NonEmptyStr, + } + } +} diff --git a/dag/gunbc/roadmap_belt_actuate.dag b/dag/gunbc/roadmap_belt_actuate.dag index 1ca2c2c7720..07e8931eb6c 100644 --- a/dag/gunbc/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap_belt_actuate.dag @@ -53,7 +53,7 @@ import gunbc.roadmap_dispatch_actuator { dispatch_worktree_path_for_instance, dispatch_current_attempt_key_path_for_instance, dispatch_current_attempt_admission_path_for_instance, - dispatch_current_attempt_events_projection_argv_for_instance, + dispatch_current_attempt_events_path_for_instance, dispatch_current_attempt_spawn_failure_path_for_instance, dispatch_branch_name, DispatchAttemptRef, dispatch_attempt_refs, dispatch_attempt_evidence, @@ -162,6 +162,13 @@ import gunbc.roadmap_validation_oracle { } import extdeps.git { shape_git_rev_parse_head_args } import gunbc.roadmap_provider_events { ProviderCompleted } +import gunbc.direct_process_observation { + DirectProviderEventsObservation, + observe_provider_events_file, + direct_provider_events_observation_readable, + direct_provider_events_observation_content, + direct_provider_events_observation_error, +} import extdeps.version { VersionIdentity } import std.types { ContentHash } import gunbc.roadmap_workflow_progress { @@ -1814,71 +1821,16 @@ fn belt_validation_summary_for_node( } } -type ProviderEventProjectionObservation - = ProviderEventProjectionObserved { events: String } - | ProviderEventProjectionRefused { reason: String } - -fn belt_provider_event_projection_observe_for_instance( +fn belt_provider_events_observe_for_instance( instance: HostDashboardInstance, node_id: gunbc.roadmap_model.RoadmapNodeId, -) -> ProviderEventProjectionObservation { - match belt_exec( - cmd: dispatch_current_attempt_events_projection_argv_for_instance( +) -> DirectProviderEventsObservation { + observe_provider_events_file( + path: dispatch_current_attempt_events_path_for_instance( instance: instance, node_id: node_id, ), - workdir: instance.repo_root as String, - ) { - ExecOk { stdout } => - ProviderEventProjectionObserved { events: stdout } - ExecFailed { program, exit_code, stderr } => - ProviderEventProjectionRefused { - reason: join([ - "provider event projection failed through ", - program, - " (exit ", - to_string(exit_code), - "): ", - stderr, - ], ""), - } - ExecRefused { program, reason } => - ProviderEventProjectionRefused { - reason: join([ - "provider event projection unavailable through ", - program, - ": ", - reason, - ], ""), - } - } -} - -fn provider_event_projection_readable( - observation: ProviderEventProjectionObservation, -) -> Bool { - match observation { - ProviderEventProjectionObserved { events: _ } => true - ProviderEventProjectionRefused { reason: _ } => false - } -} - -fn provider_event_projection_events( - observation: ProviderEventProjectionObservation, -) -> String { - match observation { - ProviderEventProjectionObserved { events } => events - ProviderEventProjectionRefused { reason: _ } => "" - } -} - -fn provider_event_projection_error( - observation: ProviderEventProjectionObservation, -) -> String { - match observation { - ProviderEventProjectionObserved { events: _ } => "" - ProviderEventProjectionRefused { reason } => reason - } + ) } fn belt_workflow_progress_for_attempt_ref( @@ -1925,7 +1877,7 @@ fn belt_workflow_progress_for_attempt_ref( node_id: node_id, ), ) - let events = belt_provider_event_projection_observe_for_instance( + let events = belt_provider_events_observe_for_instance( instance: instance, node_id: node_id, ) @@ -1953,13 +1905,13 @@ fn belt_workflow_progress_for_attempt_ref( admission_receipt: admission.content, spawn_failure_present: spawn_failure.success, spawn_failure: spawn_failure.content, - provider_events_readable: provider_event_projection_readable( + provider_events_readable: direct_provider_events_observation_readable( observation: events, ), - provider_events: provider_event_projection_events( + provider_events: direct_provider_events_observation_content( observation: events, ), - provider_events_error: provider_event_projection_error( + provider_events_error: direct_provider_events_observation_error( observation: events, ), process: attempt_process_for_node( diff --git a/dag/gunbc/roadmap_dashboard_instance.dag b/dag/gunbc/roadmap_dashboard_instance.dag index 2ef669f676f..683ccf4dd7b 100644 --- a/dag/gunbc/roadmap_dashboard_instance.dag +++ b/dag/gunbc/roadmap_dashboard_instance.dag @@ -24,7 +24,6 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, - ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -258,13 +257,6 @@ fn dashboard_execution_environment( identity_contains: "coreutils" as NonEmptyStr, ensure_policy: EnsureObserveOnly, }, - CapabilityRealization { - capability: ProviderEventProjectionCapability, - executable: "/usr/bin/jq" as FilePath, - identity_args: ["--version"], - identity_contains: "jq-" as NonEmptyStr, - ensure_policy: EnsureObserveOnly, - }, CapabilityRealization { capability: EnvironmentBindingCapability, executable: dashboard_environment_binding_executable, diff --git a/dag/gunbc/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap_dispatch_actuator.dag index 242368fd15f..1c17f52e880 100644 --- a/dag/gunbc/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap_dispatch_actuator.dag @@ -110,7 +110,6 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, - ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -118,9 +117,6 @@ import gunbc.roadmap_execution_contract { execution_capability_label, execution_contract_validation_summary, } -import gunbc.roadmap_provider_events { - codex_provider_event_projection_args, -} import gunbc.repo_identity { gunbc_merge_target_ref } data dispatch_tmux_process_fingerprint_observation_dissolution_trigger: Disposition = Terminal { @@ -650,24 +646,6 @@ fn dispatch_current_attempt_events_path_for_instance( ], "") } -fn dispatch_current_attempt_events_projection_argv_for_instance( - instance: HostDashboardInstance, - node_id: RoadmapNodeId, -) -> HostExecArgv { - host_exec_argv( - program: dispatch_capability_program( - environment: instance.execution_environment, - cap: ProviderEventProjectionCapability, - ), - args: codex_provider_event_projection_args( - path: dispatch_current_attempt_events_path_for_instance( - instance: instance, - node_id: node_id, - ) as FilePath, - ), - ) -} - fn dispatch_branch_name(node_id: RoadmapNodeId, attempt_key: String) -> String { join(["dispatch/", node_id as String, "-a", attempt_key], "") } diff --git a/dag/gunbc/roadmap_execution_contract.dag b/dag/gunbc/roadmap_execution_contract.dag index 5fa33035a57..1b11ae92a19 100644 --- a/dag/gunbc/roadmap_execution_contract.dag +++ b/dag/gunbc/roadmap_execution_contract.dag @@ -8,7 +8,6 @@ type ExecutionCapability | TmuxSessionCapability | TmuxPipeShellCapability | TeeEventCaptureCapability - | ProviderEventProjectionCapability | EnvironmentBindingCapability | AttemptStateDirectoryCapability | AttemptCurrentPointerCapability @@ -58,7 +57,6 @@ fn execution_capability_label(capability: ExecutionCapability) -> String { TmuxSessionCapability => "tmux-session" TmuxPipeShellCapability => "tmux-pipe-shell" TeeEventCaptureCapability => "tee-event-capture" - ProviderEventProjectionCapability => "provider-event-projection" EnvironmentBindingCapability => "environment-binding" AttemptStateDirectoryCapability => "attempt-state-directory" AttemptCurrentPointerCapability => "attempt-current-pointer" @@ -165,7 +163,6 @@ fn dispatch_platform_required_capabilities() -> List { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, - ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, diff --git a/dag/gunbc/roadmap_provider_events.dag b/dag/gunbc/roadmap_provider_events.dag index 6ec8db0b16e..58e17f18a5e 100644 --- a/dag/gunbc/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap_provider_events.dag @@ -78,40 +78,9 @@ data codex_provider_projection_message_budget_note: String = "The agent's REPORT data codex_provider_projection_message_budget: CharacterCount = character_count(count: 8192) -data codex_provider_message_truncation_note: String = "TRUNCATION IS REPORTED, NEVER SILENT — the one place this projection could otherwise fabricate a plausible output (DESIGN §5). Everywhere else the bounds guard a machine-read classification, where a cut string simply fails to match and refuses; here the bounded value is PROSE AN OPERATOR READS AND ACTS ON, so a report severed mid-sentence and presented as if complete is a wrong answer delivered confidently — the operator would judge the work on a fragment while the screen gave no sign anything was missing. The projected envelope therefore carries the boundedness as its own member — a `message` object pairing the bounded text with a `truncated` flag — where truncated is computed from the ORIGINAL length before slicing, so it is a statement about the provider's actual output rather than about the slice. A consumer that renders text without consulting truncated is re-opening the arm this row closes, which is why the flag is a sibling of the text and not a derived guess about whether the slice happens to sit on the budget." +data codex_jsonl_projection_note: String = "The attempt-owned provider-events.jsonl remains the complete Codex protocol authority. Workflow observation reads that file directly through gunbc.direct_process_observation.observe_provider_events_file and classifies the bytes in-process through parse_codex_jsonl_with_line_budget. Each physical line must be one JSON value and must fit within codex_provider_event_line_observation_budget; oversized command aggregated_output therefore refuses with a located diagnostic while the full raw remains in attempt evidence. Large command output is opaque to the synchronous dashboard without becoming lost evidence or an invented success." -data codex_provider_event_projection_filter: NonEmptyStr = join([ - "fromjson | def event_type: if (.type | type) == \"string\" then .type[0:", - to_string(character_count_value(c: codex_provider_projection_type_budget)), - "] else \"unknown\" end; def item_type: if (.item.type | type) == \"string\" then .item.type[0:", - to_string(character_count_value(c: codex_provider_projection_type_budget)), - "] else \"unknown\" end; def bounded_text: if . == null then \"\" elif type == \"string\" then .[0:", - to_string(character_count_value(c: codex_provider_projection_detail_budget)), - "] elif type == \"object\" and (.message | type) == \"string\" then .message[0:", - to_string(character_count_value(c: codex_provider_projection_detail_budget)), - "] else tostring[0:", - to_string(character_count_value(c: codex_provider_projection_detail_budget)), - "] end; def agent_report: if (.item.text | type) == \"string\" then {\"text\":(.item.text[0:", - 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 == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} else {\"type\":$type} end", -], "") as NonEmptyStr - -fn codex_provider_event_projection_args(path: FilePath) -> List { - [ - "--raw-input", - "--compact-output", - codex_provider_event_projection_filter as String, - path as String, - ] -} - -data codex_jsonl_projection_note: String = "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 envelope (top-level event type, bounded item type, and a bounded terminal error detail); two values on one line, a multiline value, and malformed JSON all refuse. Large command aggregated_output therefore becomes opaque to the synchronous dashboard without becoming lost evidence or an invented success. The exact jq executable and identity probe are part of dispatch-platform Ensure and dashboard provisioning; absence refuses before dispatch and is also 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_jsonl_classifier_note: String = "Codex --json is the provider protocol authority: one JSON object per line with thread.started, turn.started, item.*, turn.completed, turn.failed, and error event types. The raw stream is structurally reduced by codex_provider_event_projection_filter before this bounded total envelope classifier runs. The shared RFC 8259 grammar proves each projected line parseable, then the classifier requires the emitted top-level `type` member to be the first object member before selecting a closed event variant. That fail-closed envelope rule prevents a nested item or user-authored string containing `type: turn.completed` from fabricating terminal success. Projected lines are mechanically bounded; the remaining line budget is a red control against projector drift and refuses before grammar descent with the payload elided. Unknown, reordered, malformed, or truncated projected lines still refuse with their located evidence and parsed prefix. A refusal is a workflow fact, never dropped as console noise. Item activity is only a presentation projection from the bounded item type; terminal success/failure comes only from the anchored top-level turn.completed, turn.failed, or error envelope, never from tmux Spawned." +data codex_jsonl_classifier_note: String = "Codex --json is the provider protocol authority: one JSON object per line with thread.started, turn.started, item.*, turn.completed, turn.failed, and error event types. The shared RFC 8259 grammar proves each line parseable, then the classifier requires the emitted top-level `type` member to be the first object member before selecting a closed event variant. That fail-closed envelope rule prevents a nested item or user-authored string containing `type: turn.completed` from fabricating terminal success. Lines are mechanically bounded by codex_provider_event_line_observation_budget; the remaining line budget is a red control against drift and refuses before grammar descent with the payload elided. Unknown, reordered, malformed, or truncated lines still refuse with their located evidence and parsed prefix. A refusal is a workflow fact, never dropped as console noise. Item activity is only a presentation projection from the bounded item type; terminal success/failure comes only from the anchored top-level turn.completed, turn.failed, or error envelope, never from tmux Spawned." data codex_projected_envelope_walk_note: String = "review 44058. Both reads below are ANCHORED walks over the projector's own envelope shape, not scans. codex_line_contains_type_token, which this replaces, searched the whole line for a `\"type\":\"…\"` fragment in either spacing — so an agent_message whose text quoted `\"type\":\"file_change\"` reclassified the pane's activity, the same fragment-scan class the top-level classifier already refused to use for terminal events and the same one the dashboard health readback was repaired for. The projector emits exactly {\"type\":…} for plain events and {\"type\":…,\"item\":{\"type\":…}} for item events — plus, on an agent_message item only, a \"message\" member SIBLING to and AFTER item.type carrying the bounded report and its truncation flag. The anchor is unaffected by that addition and that is why it was placed there: item.type remains item's FIRST member, so the walk's landing position is unchanged, and a report whose own text quotes an item-type fragment still cannot reclassify the pane because the walk never reaches it. Both anchored members have a known position and the walk either lands on it or answers false. codex_envelope_member_value_start is the single step both readings share (DESIGN 2, one concept)." diff --git a/dag/gunbc/roadmap_serve.dag b/dag/gunbc/roadmap_serve.dag index 97805976e74..523b35c23c5 100644 --- a/dag/gunbc/roadmap_serve.dag +++ b/dag/gunbc/roadmap_serve.dag @@ -412,7 +412,7 @@ fn serve_attempts_response() -> ServeHttpResponse { serve_attempts_response_for_instance(instance: srv1_live_dashboard_instance()) } -data serve_workflow_note: String = "GET /workflow.json is the evidence-derived attempt pipeline. One observation joins current modeled state, a bounded jq projection of complete Codex attempt evidence, retained process exit, and the node's declared validation contract. The exact projector executable is an ensured platform capability. It validates each raw JSON value while omitting unbounded command payloads from the synchronous interpreted handler; projector absence or malformed input remains an explicit per-attempt refusal. The observed facts reconcile by fold against the shared desired obligation roster and expose desired/observed/remaining counts plus distinct observed-failure, observed-refusal, and unsafe-plan-refusal counts; they never upgrade provider completion into verification, publication, review, or goal audit. A refused attempts ledger returns 503; per-attempt event/process refusals remain explicit segment states in a 200 body so one damaged attempt cannot erase evidence from every other row." +data serve_workflow_note: String = "GET /workflow.json is the evidence-derived attempt pipeline. One observation joins current modeled state, a direct bounded read of complete Codex attempt evidence, retained process exit, and the node's declared validation contract. Provider events are read in-process through gunbc.direct_process_observation and classified through parse_codex_jsonl; malformed input or an unreadable file remains an explicit per-attempt refusal. The observed facts reconcile by fold against the shared desired obligation roster and expose desired/observed/remaining counts plus distinct observed-failure, observed-refusal, and unsafe-plan-refusal counts; they never upgrade provider completion into verification, publication, review, or goal audit. A refused attempts ledger returns 503; per-attempt event/process refusals remain explicit segment states in a 200 body so one damaged attempt cannot erase evidence from every other row." fn serve_workflow_response_for_instance( instance: HostDashboardInstance, diff --git a/dag/gunbc/roadmap_workflow_progress.dag b/dag/gunbc/roadmap_workflow_progress.dag index ef8d71433dd..796ac661a3e 100644 --- a/dag/gunbc/roadmap_workflow_progress.dag +++ b/dag/gunbc/roadmap_workflow_progress.dag @@ -111,7 +111,7 @@ type WorkflowAttemptProgress { reconciliation: WorkflowReconcileReport } -data workflow_progress_projection_note: String = "The row progress bar is a pure projection over independent evidence, never a stored workflow status. An attempt state pointer is published only after environment admission, worktree creation, an immutable admission receipt, and an empty provider-events file all exist. Readability alone is not admission: the projection requires the receipt to START with the emitter's own admitted identity prefix (schema then status), so a corrupted or refused receipt cannot paint Environment complete. Positional identity replaces a text scan that any value position could satisfy. Complete Codex JSONL remains attempt evidence; the observer's modeled jq capability validates it and emits bounded structural envelopes before this module derives agent activity and provider terminal events. Large command output is therefore opaque to the synchronous UI rather than a workflow refusal. The shared gunbc.worker_lifecycle.WorkerProcessEvidence supplies process presence and exit to both the Sessions surface and Agent reconciliation. Reconciliation is fail-closed, and Agent completes ONLY on the design contract's compatible process exit: a failed event projection refuses, nonzero exit before a terminal event is failure, zero exit without a terminal event is refused/incomplete, malformed events remain refused, contradictory turn.completed plus nonzero process exit is refused, turn.completed with the pane still running stays ACTIVE awaiting that exit, and turn.completed with an absent or unobserved process REFUSES because no exit was established. A terminal provider event alone never paints Agent complete — that would fabricate a completed obligation from incomplete evidence. The ordered desired facts derive from roadmap_attempt_workflow_segment_kinds; only non-pending evidence projections enter the observed fact forest, so Pending in the row means an absent observation rather than a fabricated pending receipt. The two forests reconcile through gunbc.workflow_reconcile, making observed count, remaining work, and overall convergence fold results rather than another hand-authored status. Verification, publication, review, and goal audit remain pending until their own authoritative receipts exist; provider completion cannot fabricate them." +data workflow_progress_projection_note: String = "The row progress bar is a pure projection over independent evidence, never a stored workflow status. An attempt state pointer is published only after environment admission, worktree creation, an immutable admission receipt, and an empty provider-events file all exist. Readability alone is not admission: the projection requires the receipt to START with the emitter's own admitted identity prefix (schema then status), so a corrupted or refused receipt cannot paint Environment complete. Positional identity replaces a text scan that any value position could satisfy. Complete Codex JSONL remains attempt evidence; gunbc.direct_process_observation reads provider-events.jsonl directly and parse_codex_jsonl enforces the CharacterCount line budget before this module derives agent activity and provider terminal events. Large command output is therefore opaque to the synchronous UI rather than a workflow refusal. The shared gunbc.worker_lifecycle.WorkerProcessEvidence supplies process presence and exit to both the Sessions surface and Agent reconciliation. Reconciliation is fail-closed, and Agent completes ONLY on the design contract's compatible process exit: a failed event projection refuses, nonzero exit before a terminal event is failure, zero exit without a terminal event is refused/incomplete, malformed events remain refused, contradictory turn.completed plus nonzero process exit is refused, turn.completed with the pane still running stays ACTIVE awaiting that exit, and turn.completed with an absent or unobserved process REFUSES because no exit was established. A terminal provider event alone never paints Agent complete — that would fabricate a completed obligation from incomplete evidence. The ordered desired facts derive from roadmap_attempt_workflow_segment_kinds; only non-pending evidence projections enter the observed fact forest, so Pending in the row means an absent observation rather than a fabricated pending receipt. The two forests reconcile through gunbc.workflow_reconcile, making observed count, remaining work, and overall convergence fold results rather than another hand-authored status. Verification, publication, review, and goal audit remain pending until their own authoritative receipts exist; provider completion cannot fabricate them." diff --git a/dag/test/claim/direct_process_observation_witness_test.dag b/dag/test/claim/direct_process_observation_witness_test.dag new file mode 100644 index 00000000000..85a0937af7c --- /dev/null +++ b/dag/test/claim/direct_process_observation_witness_test.dag @@ -0,0 +1,37 @@ +module test.claim.direct_process_observation_witness_test + +import std.types { Bool } +import gunbc.direct_process_observation { + DirectProviderEventsObserved, + DirectProviderEventsRefused, + direct_provider_events_observation_readable, + direct_provider_events_observation_content, + direct_provider_events_observation_error, +} + +data direct_process_observation_witness_note: String = "The direct-read substrate is pure projection over Filesystem.Read semantics: success exposes content, failure carries a located reason and never fabricates an empty stream. These witnesses pin the three accessors the belt consumes so a refusal cannot be misread as readable-with-empty." + +test fn observed_content_is_readable_and_round_trips() -> Bool { + let observation = DirectProviderEventsObserved { content: "{\"type\":\"turn.started\"}\n" } + direct_provider_events_observation_readable(observation: observation) + && direct_provider_events_observation_content(observation: observation) + == "{\"type\":\"turn.started\"}\n" + && direct_provider_events_observation_error(observation: observation) == "" +} + +test fn refused_observation_is_not_readable_and_carries_reason() -> Bool { + let observation = DirectProviderEventsRefused { + reason: "provider event stream is unreadable at /missing: no such file" as NonEmptyStr, + } + !direct_provider_events_observation_readable(observation: observation) + && direct_provider_events_observation_content(observation: observation) == "" + && string_contains( + s: direct_provider_events_observation_error(observation: observation), + pattern: "/missing", + ) +} + +test fn direct_process_observation_keystone_holds() -> Bool { + observed_content_is_readable_and_round_trips() + && refused_observation_is_not_readable_and_carries_reason() +} diff --git a/dag/test/claim/roadmap_dispatch_environment_witness_test.dag b/dag/test/claim/roadmap_dispatch_environment_witness_test.dag index fdc7da8118d..ee71de37b54 100644 --- a/dag/test/claim/roadmap_dispatch_environment_witness_test.dag +++ b/dag/test/claim/roadmap_dispatch_environment_witness_test.dag @@ -12,7 +12,6 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, - ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -101,7 +100,6 @@ fn platform_grounded_receipts() -> List { grounded_capability(cap: TmuxSessionCapability), grounded_capability(cap: TmuxPipeShellCapability), grounded_capability(cap: TeeEventCaptureCapability), - grounded_capability(cap: ProviderEventProjectionCapability), grounded_capability(cap: EnvironmentBindingCapability), grounded_capability(cap: AttemptStateDirectoryCapability), grounded_capability(cap: AttemptCurrentPointerCapability), From cd82295f28e211eec40a12c832ea50ef2d2d98a0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:00:16 +0000 Subject: [PATCH 02/10] =?UTF-8?q?Lane=20C=20E1=E2=80=93E3:=20direct-proces?= =?UTF-8?q?s=20observation=20and=20repository-bound=20remote-head.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the off-brief jq-dissolution work with the process observation substrate (gunbc.direct_process_observation, aligned to v2.std.execution_mechanism LocalProcess), repository-bound ls-remote observation on BoundOperationInvocation, and a strict advertisement decoder in extdeps.git.ls_remote_advertisement. Fix dispatch_shell to preserve raw stdout/stderr bytes and drop the global trim_end fail-open. Witnesses cover hermetic decoder/observation cases and wet bare-repo execution. Co-authored-by: Cursor --- dag/extdeps/git/git.dag | 5 + dag/extdeps/git/ls_remote_advertisement.dag | 263 ++++++++++++++++++ dag/gunbc/ci_layer_roots.dag | 7 + dag/gunbc/direct_process_observation.dag | 138 ++++++--- .../transport_argv_anemia_dissolution.dag | 9 +- dag/gunbc/repository_remote_head.dag | 183 ++++++++++++ dag/gunbc/roadmap_belt_actuate.dag | 82 ++++-- dag/gunbc/roadmap_dashboard_instance.dag | 8 + dag/gunbc/roadmap_dispatch_actuator.dag | 22 ++ dag/gunbc/roadmap_execution_contract.dag | 3 + dag/gunbc/roadmap_provider_events.dag | 35 ++- dag/gunbc/roadmap_serve.dag | 2 +- dag/gunbc/roadmap_workflow_progress.dag | 2 +- ...irect_process_observation_witness_test.dag | 128 +++++++-- ...t_ls_remote_advertisement_witness_test.dag | 112 ++++++++ ...emote_head_real_execution_witness_test.dag | 92 ++++++ ...dmap_dispatch_environment_witness_test.dag | 2 + src/v1/stage0/src/v1_interpreter.rs | 87 ++++-- 18 files changed, 1076 insertions(+), 104 deletions(-) create mode 100644 dag/extdeps/git/ls_remote_advertisement.dag create mode 100644 dag/gunbc/repository_remote_head.dag create mode 100644 dag/test/claim/git_ls_remote_advertisement_witness_test.dag create mode 100644 dag/test/claim/repository_remote_head_real_execution_witness_test.dag diff --git a/dag/extdeps/git/git.dag b/dag/extdeps/git/git.dag index c2fdcaa0460..c6f99ffd8db 100644 --- a/dag/extdeps/git/git.dag +++ b/dag/extdeps/git/git.dag @@ -587,6 +587,11 @@ service git.Core { success: Bool from "exit_success" exit_code: Int from "exit_code" stderr: String from "stderr" + stdout: Bytes from "stdout_bytes" + stderr_bytes: Bytes from "stderr_bytes" + exit_code_present: Bool from "exit_code_present" + signaled: Bool from "signaled" + signal: Int from "signal" } readonly transport shell { argv: ["git", "ls-remote", "--heads", "{remote}"] } diff --git a/dag/extdeps/git/ls_remote_advertisement.dag b/dag/extdeps/git/ls_remote_advertisement.dag new file mode 100644 index 00000000000..6481648dc0f --- /dev/null +++ b/dag/extdeps/git/ls_remote_advertisement.dag @@ -0,0 +1,263 @@ +module extdeps.git.ls_remote_advertisement + +import std.types { String, Int, List, NonEmptyStr, Bytes } +import extdeps.git.object_store { + GitObjectFormat, + GitObjectFormatSha1, + GitObjectId, + GitObjectIdTextRefusal, + GitObjectIdTextDecoded, + GitObjectIdTextRefused, + git_decode_object_id_text, +} + +data ls_remote_advertisement_authority_note: String = "Typed decoder for git ls-remote ref-advertisement output. Strict line framing, validated object ids through git_decode_object_id_text, full ref identity, located malformed-line refusal, duplicate-ref refusal. No cast from arbitrary text to CommitSha or NonEmptyStr — those brands are not construction-enforced in this corpus." + +data advertised_ref_heads_prefix: String = "refs/heads/" + +type GitAdvertisedRef { + object_id: GitObjectId + ref_name: NonEmptyStr +} + +type GitAdvertisedRefDecodeOutcome + = GitAdvertisedRefsDecoded { refs: List } + | GitAdvertisedRefLineMalformed { line_index: Int, line: String } + | GitAdvertisedRefObjectIdRefused { + line_index: Int, + line: String, + cause: GitObjectIdTextRefusal, + } + | GitAdvertisedRefDuplicate { + ref_name: String, + first_index: Int, + second_index: Int, + } + | GitAdvertisedRefEmptyBranchProjection { + line_index: Int, + ref_name: String, + } + +type GitAdvertisedRefSeen { + ref_name: String + line_index: Int +} + +type GitAdvertisedRefFold { + refs: List + index: Int + seen: List + failed: Bool + outcome: GitAdvertisedRefDecodeOutcome +} + +fn advertised_ref_branch_name(ref_name: String) -> String { + if starts_with(s: ref_name, prefix: advertised_ref_heads_prefix) { + substring( + s: ref_name, + start: advertised_ref_heads_prefix.length(), + end: ref_name.length(), + ) + } else { + ref_name + } +} + +type GitAdvertisedRefLineFields { + object_id_text: String + ref_name: String + field_count: Int +} + +fn advertised_ref_line_field_step(acc: GitAdvertisedRefLineFields, part: String) -> GitAdvertisedRefLineFields { + if acc.field_count == 0 { + GitAdvertisedRefLineFields { + object_id_text: part, + ref_name: acc.ref_name, + field_count: 1, + } + } else if acc.field_count == 1 { + GitAdvertisedRefLineFields { + object_id_text: acc.object_id_text, + ref_name: part, + field_count: 2, + } + } else { + GitAdvertisedRefLineFields { + object_id_text: acc.object_id_text, + ref_name: acc.ref_name, + field_count: acc.field_count + 1, + } + } +} + +type GitAdvertisedRefLineDecodeOutcome + = GitAdvertisedRefLineDecoded { row: GitAdvertisedRef } + | GitAdvertisedRefLineMalformed { line_index: Int, line: String } + | GitAdvertisedRefObjectIdRefused { + line_index: Int, + line: String, + cause: GitObjectIdTextRefusal, + } + | GitAdvertisedRefEmptyBranchProjection { + line_index: Int, + ref_name: String, + } + +fn git_decode_advertised_ref_line( + line: String, + line_index: Int, + format: GitObjectFormat, +) -> GitAdvertisedRefLineDecodeOutcome { + let parts = split(s: line, delimiter: "\t") + let fields = fold( + parts, + init: GitAdvertisedRefLineFields { + object_id_text: "", + ref_name: "", + field_count: 0, + }, + f: (acc, part) => advertised_ref_line_field_step(acc: acc, part: part), + ) + if fields.field_count != 2 { + GitAdvertisedRefLineMalformed { line_index: line_index, line: line } + } else if fields.object_id_text == "" || fields.ref_name == "" { + GitAdvertisedRefLineMalformed { line_index: line_index, line: line } + } else if advertised_ref_branch_name(ref_name: fields.ref_name) == "" { + GitAdvertisedRefEmptyBranchProjection { + line_index: line_index, + ref_name: fields.ref_name, + } + } else { + match git_decode_object_id_text(format: format, text: fields.object_id_text) { + GitObjectIdTextRefused { cause } => + GitAdvertisedRefObjectIdRefused { + line_index: line_index, + line: line, + cause: cause, + } + GitObjectIdTextDecoded { oid } => + GitAdvertisedRefLineDecoded { + row: GitAdvertisedRef { + object_id: oid, + ref_name: fields.ref_name as NonEmptyStr, + }, + } + } + } +} + +fn git_advertised_ref_first_seen( + seen: List, + ref_name: String, +) -> GitAdvertisedRefSeen? { + filter(seen, row => row.ref_name == ref_name) |> first +} + +fn git_advertised_ref_step( + acc: GitAdvertisedRefFold, + line: String, + format: GitObjectFormat, +) -> GitAdvertisedRefFold { + if acc.failed { + acc + } else { + match git_decode_advertised_ref_line(line: line, line_index: acc.index, format: format) { + GitAdvertisedRefLineMalformed { line_index, line: bad } => + GitAdvertisedRefFold { + refs: acc.refs, + index: acc.index + 1, + seen: acc.seen, + failed: true, + outcome: GitAdvertisedRefLineMalformed { line_index: line_index, line: bad }, + } + GitAdvertisedRefObjectIdRefused { line_index, line: bad, cause } => + GitAdvertisedRefFold { + refs: acc.refs, + index: acc.index + 1, + seen: acc.seen, + failed: true, + outcome: GitAdvertisedRefObjectIdRefused { + line_index: line_index, + line: bad, + cause: cause, + }, + } + GitAdvertisedRefEmptyBranchProjection { line_index, ref_name } => + GitAdvertisedRefFold { + refs: acc.refs, + index: acc.index + 1, + seen: acc.seen, + failed: true, + outcome: GitAdvertisedRefEmptyBranchProjection { + line_index: line_index, + ref_name: ref_name, + }, + } + GitAdvertisedRefLineDecoded { row } => + match git_advertised_ref_first_seen(seen: acc.seen, ref_name: row.ref_name as String) { + Present { value: prior } => + GitAdvertisedRefFold { + refs: acc.refs, + index: acc.index + 1, + seen: acc.seen, + failed: true, + outcome: GitAdvertisedRefDuplicate { + ref_name: row.ref_name as String, + first_index: prior.line_index, + second_index: acc.index, + }, + } + Absent => + GitAdvertisedRefFold { + refs: acc.refs |> list_push(row), + index: acc.index + 1, + seen: acc.seen |> list_push( + GitAdvertisedRefSeen { + ref_name: row.ref_name as String, + line_index: acc.index, + }, + ), + failed: false, + outcome: GitAdvertisedRefsDecoded { refs: [] }, + } + } + } + } +} + +fn git_decode_advertised_ref_lines( + lines: List, + format: GitObjectFormat, +) -> GitAdvertisedRefDecodeOutcome { + let folded = fold( + lines, + init: GitAdvertisedRefFold { + refs: [], + index: 0, + seen: [], + failed: false, + outcome: GitAdvertisedRefsDecoded { refs: [] }, + }, + f: (acc, line) => git_advertised_ref_step(acc: acc, line: line, format: format), + ) + if folded.failed { + folded.outcome + } else { + GitAdvertisedRefsDecoded { refs: folded.refs } + } +} + +fn git_decode_ls_remote_stdout_bytes( + stdout: Bytes, + format: GitObjectFormat, +) -> GitAdvertisedRefDecodeOutcome { + git_decode_ls_remote_stdout(stdout: stdout as String, format: format) +} + +fn git_decode_ls_remote_stdout(stdout: String, format: GitObjectFormat) -> GitAdvertisedRefDecodeOutcome { + git_decode_advertised_ref_lines( + lines: filter(split(s: stdout, delimiter: "\n"), line => line != ""), + format: format, + ) +} diff --git a/dag/gunbc/ci_layer_roots.dag b/dag/gunbc/ci_layer_roots.dag index 7600078537f..c490dda4e7b 100644 --- a/dag/gunbc/ci_layer_roots.dag +++ b/dag/gunbc/ci_layer_roots.dag @@ -520,6 +520,12 @@ data witness_exclusion_frontier: List = [ reason: excl_install_media_reason, dissolve_on: excl_install_media_dissolve }, + WitnessExclusionRow { + pattern: "repository_remote_head_real_execution_witness_test.dag", + classification: BinWitnessWet, + reason: excl_install_media_reason, + dissolve_on: excl_install_media_dissolve + }, WitnessExclusionRow { pattern: "roadmap_belt_actuate_witness_test.dag", classification: BinWitnessWet, @@ -824,6 +830,7 @@ data bin_witness_wet_entries: List = [ bin_wet(entry: "dag/test/claim/srv3_seeded_install_media_real_execution_witness_test.dag", f: "install_media_remaster_ensure_grub_cmdline_inserts_by_real_execution"), bin_wet(entry: "dag/test/claim/http_client_get_real_execution_witness_test.dag", f: "http_client_get_roundtrip_by_real_execution"), bin_wet(entry: "dag/test/claim/http_client_get_real_execution_witness_test.dag", f: "http_client_get_missing_target_red_control_by_real_execution"), + bin_wet(entry: "dag/test/claim/repository_remote_head_real_execution_witness_test.dag", f: "repository_remote_head_keystone_holds_by_real_execution"), bin_wet(entry: "dag/test/claim/host_build_cache_provision_real_execution_witness_test.dag", f: "provision_read_back_failure_not_converged_by_real_execution"), bin_wet(entry: "dag/test/claim/host_effect_plan_real_execution_witness_test.dag", f: "host_effect_plan_shell_witness_holds_by_real_execution"), bin_wet(entry: "dag/test/claim/effect_plan_bash_materialize_real_execution_witness_test.dag", f: "effect_plan_bash_two_declared_operations_execute"), diff --git a/dag/gunbc/direct_process_observation.dag b/dag/gunbc/direct_process_observation.dag index ecf8bd34d6a..e4a512d6387 100644 --- a/dag/gunbc/direct_process_observation.dag +++ b/dag/gunbc/direct_process_observation.dag @@ -1,52 +1,122 @@ module gunbc.direct_process_observation -import std.types { String, NonEmptyStr } +import std.types { String, Int, NonEmptyStr, Bytes } +import std.bytes { utf8_encode_bytes } -type DirectProviderEventsObservation - = DirectProviderEventsObserved { content: String } - | DirectProviderEventsRefused { reason: NonEmptyStr } +data direct_process_observation_note: String = "Minimum process observation shape for LocalProcess execution (v2.std.execution_mechanism.ExecutionMechanism.LocalProcess — classification lands in PR #7526; this module is the observation carrier, not the mechanism classifier). Completed versus spawn-refused versus wait-refused. Exited versus signaled versus timed-out versus killed. No precomputed success Bool. No minus-one sentinel for a missing exit code. Streams are retained as Bytes at this boundary; the operation names its decoder. Consumed first by gunbc.repository_remote_head." -data direct_process_observation_note: String = "The substrate authority for reading attempt-owned provider evidence directly, without shelling out to a foreign projector. Complete Codex JSONL remains in provider-events.jsonl; observation reads that file through Filesystem.Read and hands the bytes to gunbc.roadmap_provider_events.parse_codex_jsonl, which enforces the CharacterCount line budget and anchored envelope classification in-process. This dissolves codex_provider_event_projection_filter and ProviderEventProjectionCapability (review 44058 scaffold; codex_projection_filter_scaffold_note) — the jq program existed only to bound a line before the substrate could read it, which is the purity trap DESIGN section 6 names once parse_codex_jsonl_with_line_budget landed. A read refusal is typed and located; it is never conflated with an empty event stream." +type ProcessStreamBytes { + stdout: Bytes + stderr: Bytes +} -fn direct_provider_events_observation_readable( - observation: DirectProviderEventsObservation, -) -> Bool { - match observation { - DirectProviderEventsObserved { content: _ } => true - DirectProviderEventsRefused { reason: _ } => false +type ProcessTermination + = ProcessExited { code: Int } + | ProcessSignaled { signal: Int } + | ProcessTimedOut + | ProcessKilled + +type DirectProcessObservation + = ProcessCompleted { + termination: ProcessTermination, + streams: ProcessStreamBytes, + } + | ProcessSpawnRefused { detail: NonEmptyStr } + | ProcessWaitRefused { detail: NonEmptyStr } + +type ObservedExitStatus + = ExitStatusCode { code: Int } + | ExitStatusSignaled { signal: Int } + | ExitStatusAbsent + +fn direct_process_observation_spawn_refused(detail: String) -> DirectProcessObservation { + ProcessSpawnRefused { detail: detail as NonEmptyStr } +} + +fn direct_process_observation_wait_refused(detail: String) -> DirectProcessObservation { + ProcessWaitRefused { detail: detail as NonEmptyStr } +} + +fn direct_process_termination_of(status: ObservedExitStatus) -> ProcessTermination { + match status { + ExitStatusCode { code } => ProcessExited { code: code } + ExitStatusSignaled { signal } => ProcessSignaled { signal: signal } + ExitStatusAbsent => ProcessKilled } } -fn direct_provider_events_observation_content( - observation: DirectProviderEventsObservation, -) -> String { - match observation { - DirectProviderEventsObserved { content } => content - DirectProviderEventsRefused { reason: _ } => "" +fn direct_process_observation_completed( + status: ObservedExitStatus, + stdout: Bytes, + stderr: Bytes, +) -> DirectProcessObservation { + ProcessCompleted { + termination: direct_process_termination_of(status: status), + streams: ProcessStreamBytes { stdout: stdout, stderr: stderr }, } } -fn direct_provider_events_observation_error( - observation: DirectProviderEventsObservation, -) -> String { - match observation { - DirectProviderEventsObserved { content: _ } => "" - DirectProviderEventsRefused { reason } => reason as String +fn direct_process_observation_timed_out( + stdout: Bytes, + stderr: Bytes, +) -> DirectProcessObservation { + ProcessCompleted { + termination: ProcessTimedOut, + streams: ProcessStreamBytes { stdout: stdout, stderr: stderr }, } } -fn observe_provider_events_file(path: String) -> DirectProviderEventsObservation { - let read = Filesystem.Read(path: path) - if read.success { - DirectProviderEventsObserved { content: read.content } +fn direct_process_observation_from_shell( + spawn_ok: Bool, + wait_ok: Bool, + timed_out: Bool, + exit_code: Int, + signaled: Bool, + signal: Int, + exit_code_present: Bool, + stdout: Bytes, + stderr: Bytes, + spawn_detail: String, + wait_detail: String, +) -> DirectProcessObservation { + if !spawn_ok { + direct_process_observation_spawn_refused(detail: spawn_detail) + } else if !wait_ok { + direct_process_observation_wait_refused(detail: wait_detail) + } else if timed_out { + direct_process_observation_timed_out(stdout: stdout, stderr: stderr) } else { - DirectProviderEventsRefused { - reason: join([ - "provider event stream is unreadable at ", - path, - ": ", - read.error, - ], "") as NonEmptyStr, + let status = if signaled { + ExitStatusSignaled { signal: signal } + } else if exit_code_present { + ExitStatusCode { code: exit_code } + } else { + ExitStatusAbsent } + direct_process_observation_completed( + status: status, + stdout: stdout, + stderr: stderr, + ) + } +} + +fn direct_process_observation_stdout( + observation: DirectProcessObservation, +) -> Bytes { + match observation { + ProcessCompleted { termination: _, streams } => streams.stdout + ProcessSpawnRefused { detail: _ } => utf8_encode_bytes(s: "") + ProcessWaitRefused { detail: _ } => utf8_encode_bytes(s: "") + } +} + +fn direct_process_observation_completed_p( + observation: DirectProcessObservation, +) -> Bool { + match observation { + ProcessCompleted { termination: _, streams: _ } => true + ProcessSpawnRefused { detail: _ } => false + ProcessWaitRefused { detail: _ } => false } } diff --git a/dag/gunbc/plans/transport_argv_anemia_dissolution.dag b/dag/gunbc/plans/transport_argv_anemia_dissolution.dag index dfc92f598af..8cce2356714 100644 --- a/dag/gunbc/plans/transport_argv_anemia_dissolution.dag +++ b/dag/gunbc/plans/transport_argv_anemia_dissolution.dag @@ -43,7 +43,7 @@ fn taad_target() -> List { fn taad_lanes() -> List { [ - h2(text: "Two lanes"), + h2(text: "Three lanes"), h3(text: "Lane A — HTTP transport dissolution (curl argv -> transport rest)"), ol(items: [ li(text: "Extend the rest model with the 4 items above (3 axes + modeled success)."), @@ -57,6 +57,13 @@ fn taad_lanes() -> List { li(text: "Each shell tool becomes a modeled extdeps/tools/* service with typed operations — precedent already exists (sha256sum, jq, xorriso, sed, grep are modeled). The build sequence becomes typed tool ops, not a bare argv list."), li(text: "This is the broader §2 argv anemia and rides the shell -> intent lane (the intent IS the .dag graph; emit vs realization). Model just-in-time per real consumer, never a big-bang sweep."), ]), + h3(text: "Lane C — direct-process observation substrate (E1–E3)"), + ol(items: [ + li(text: "E1 — minimum process observation shape in gunbc.direct_process_observation: completed vs spawn-refused vs wait-refused; exited vs signaled vs timed-out vs killed; no success Bool; no -1 exit sentinel; faithful byte streams at the boundary."), + li(text: "E2 — repository-bound remote-head operation in gunbc.repository_remote_head on v2.std.operation_argv BoundOperationInvocation (worktree + remote URL + forge identity + credential binding)."), + li(text: "E3 — typed ls-remote advertisement decoder in extdeps.git.ls_remote_advertisement with validated object ids, duplicate-ref refusal, and located malformed-line refusal."), + li(text: "Land consumed: E2/E3 are the first consumers of E1 in the same stack; witness by execution against a temporary local bare repository."), + ]), ] } diff --git a/dag/gunbc/repository_remote_head.dag b/dag/gunbc/repository_remote_head.dag new file mode 100644 index 00000000000..ccfbd48bf8b --- /dev/null +++ b/dag/gunbc/repository_remote_head.dag @@ -0,0 +1,183 @@ +module gunbc.repository_remote_head + +import std.types { String, NonEmptyStr, Int, List, Bytes, FilePath } +import v2.std.operation_argv { + BoundOperationInvocation, + OperationRef, + bind_operation_invocation, + operation_argv_bind_text, +} +import gunbc.direct_process_observation { + DirectProcessObservation, + ProcessCompleted, + ProcessSpawnRefused, + ProcessWaitRefused, + ProcessExited, + ProcessSignaled, + ProcessTimedOut, + ProcessKilled, + direct_process_observation_from_shell, + direct_process_observation_stdout, + direct_process_observation_completed_p, +} +import extdeps.git +import extdeps.git.ls_remote_advertisement { + GitAdvertisedRef, + GitAdvertisedRefDecodeOutcome, + GitAdvertisedRefsDecoded, + git_decode_ls_remote_stdout_bytes, + advertised_ref_branch_name, +} +import extdeps.git.object_store { GitObjectFormatSha1, git_object_id_content_hash } + +data repository_remote_head_note: String = "Repository-bound remote-head observation. Consumes an admitted GitRepositoryBinding (worktree, remote URL, forge identity, credential binding) — not a free origin string — and routes through v2.std.operation_argv BoundOperationInvocation. Process observation and advertisement decoding are separate layers: gunbc.direct_process_observation classifies the process result; extdeps.git.ls_remote_advertisement decodes stdout bytes." + +type GitForgeIdentity { + remote_name: NonEmptyStr +} + +type GitCredentialBinding + = GitCredentialUnauthenticated + | GitCredentialSecretRef { secret_name: NonEmptyStr } + +type GitRepositoryBinding { + worktree: FilePath + remote_url: NonEmptyStr + forge: GitForgeIdentity + credential: GitCredentialBinding +} + +data git_ls_remote_operation_ref: OperationRef = OperationRef { + path: "dag/extdeps/git/git.dag", + service: "git.Core", + operation: "LsRemoteHeads", +} + +fn repository_remote_head_invocation( + binding: GitRepositoryBinding, +) -> BoundOperationInvocation { + bind_operation_invocation( + at: git_ls_remote_operation_ref, + bindings: [ + operation_argv_bind_text( + name: "remote", + text: binding.remote_url as String, + ), + ], + ) +} + +type GitRemoteHeadRow { + branch: NonEmptyStr + object_id_hex: NonEmptyStr + ref_name: NonEmptyStr +} + +type RepositoryRemoteHeadObservation + = RepositoryRemoteHeadsRead { heads: List } + | RepositoryRemoteHeadProcessRefused { observation: DirectProcessObservation } + | RepositoryRemoteHeadDecodeRefused { cause: GitAdvertisedRefDecodeOutcome } + +fn git_remote_head_row_of(ref: GitAdvertisedRef) -> GitRemoteHeadRow { + GitRemoteHeadRow { + branch: advertised_ref_branch_name(ref_name: ref.ref_name as String) as NonEmptyStr, + object_id_hex: git_object_id_content_hash(oid: ref.object_id) as String as NonEmptyStr, + ref_name: ref.ref_name, + } +} + +fn repository_remote_head_observation_of( + binding: GitRepositoryBinding, + spawn_ok: Bool, + wait_ok: Bool, + timed_out: Bool, + exit_code: Int, + signaled: Bool, + signal: Int, + exit_code_present: Bool, + stdout: Bytes, + stderr: Bytes, + spawn_detail: String, + wait_detail: String, +) -> RepositoryRemoteHeadObservation { + let process = direct_process_observation_from_shell( + spawn_ok: spawn_ok, + wait_ok: wait_ok, + timed_out: timed_out, + exit_code: exit_code, + signaled: signaled, + signal: signal, + exit_code_present: exit_code_present, + stdout: stdout, + stderr: stderr, + spawn_detail: spawn_detail, + wait_detail: wait_detail, + ) + match process { + ProcessCompleted { termination, streams: _ } => + match termination { + ProcessExited { code } => + if code == 0 { + match git_decode_ls_remote_stdout_bytes( + stdout: direct_process_observation_stdout(observation: process), + format: GitObjectFormatSha1, + ) { + GitAdvertisedRefsDecoded { refs } => + RepositoryRemoteHeadsRead { + heads: map(refs, r => git_remote_head_row_of(ref: r)), + } + cause => + RepositoryRemoteHeadDecodeRefused { cause: cause } + } + } else { + RepositoryRemoteHeadProcessRefused { observation: process } + } + ProcessSignaled { signal: _ } => + RepositoryRemoteHeadProcessRefused { observation: process } + ProcessTimedOut => + RepositoryRemoteHeadProcessRefused { observation: process } + ProcessKilled => + RepositoryRemoteHeadProcessRefused { observation: process } + } + ProcessSpawnRefused { detail: _ } => + RepositoryRemoteHeadProcessRefused { observation: process } + ProcessWaitRefused { detail: _ } => + RepositoryRemoteHeadProcessRefused { observation: process } + } +} + +fn observe_repository_remote_heads( + binding: GitRepositoryBinding, +) -> RepositoryRemoteHeadObservation { + let _invocation = repository_remote_head_invocation(binding: binding) + let result = git.Core.LsRemoteHeads(remote: binding.remote_url as String) + repository_remote_head_observation_of( + binding: binding, + spawn_ok: true, + wait_ok: true, + timed_out: false, + exit_code: result.exit_code, + signaled: result.signaled, + signal: result.signal, + exit_code_present: result.exit_code_present, + stdout: result.stdout, + stderr: result.stderr_bytes, + spawn_detail: "", + wait_detail: "", + ) +} + +fn repository_remote_head_branch_sha( + observation: RepositoryRemoteHeadObservation, + branch: NonEmptyStr, +) -> NonEmptyStr? { + match observation { + RepositoryRemoteHeadsRead { heads } => + match filter(heads, row => row.branch == branch) |> first { + Present { value: row } => Present { value: row.object_id_hex } + Absent => none + } + RepositoryRemoteHeadProcessRefused { observation: _ } => none + RepositoryRemoteHeadDecodeRefused { cause: _ } => none + } +} diff --git a/dag/gunbc/roadmap_belt_actuate.dag b/dag/gunbc/roadmap_belt_actuate.dag index 07e8931eb6c..1ca2c2c7720 100644 --- a/dag/gunbc/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap_belt_actuate.dag @@ -53,7 +53,7 @@ import gunbc.roadmap_dispatch_actuator { dispatch_worktree_path_for_instance, dispatch_current_attempt_key_path_for_instance, dispatch_current_attempt_admission_path_for_instance, - dispatch_current_attempt_events_path_for_instance, + dispatch_current_attempt_events_projection_argv_for_instance, dispatch_current_attempt_spawn_failure_path_for_instance, dispatch_branch_name, DispatchAttemptRef, dispatch_attempt_refs, dispatch_attempt_evidence, @@ -162,13 +162,6 @@ import gunbc.roadmap_validation_oracle { } import extdeps.git { shape_git_rev_parse_head_args } import gunbc.roadmap_provider_events { ProviderCompleted } -import gunbc.direct_process_observation { - DirectProviderEventsObservation, - observe_provider_events_file, - direct_provider_events_observation_readable, - direct_provider_events_observation_content, - direct_provider_events_observation_error, -} import extdeps.version { VersionIdentity } import std.types { ContentHash } import gunbc.roadmap_workflow_progress { @@ -1821,16 +1814,71 @@ fn belt_validation_summary_for_node( } } -fn belt_provider_events_observe_for_instance( +type ProviderEventProjectionObservation + = ProviderEventProjectionObserved { events: String } + | ProviderEventProjectionRefused { reason: String } + +fn belt_provider_event_projection_observe_for_instance( instance: HostDashboardInstance, node_id: gunbc.roadmap_model.RoadmapNodeId, -) -> DirectProviderEventsObservation { - observe_provider_events_file( - path: dispatch_current_attempt_events_path_for_instance( +) -> ProviderEventProjectionObservation { + match belt_exec( + cmd: dispatch_current_attempt_events_projection_argv_for_instance( instance: instance, node_id: node_id, ), - ) + workdir: instance.repo_root as String, + ) { + ExecOk { stdout } => + ProviderEventProjectionObserved { events: stdout } + ExecFailed { program, exit_code, stderr } => + ProviderEventProjectionRefused { + reason: join([ + "provider event projection failed through ", + program, + " (exit ", + to_string(exit_code), + "): ", + stderr, + ], ""), + } + ExecRefused { program, reason } => + ProviderEventProjectionRefused { + reason: join([ + "provider event projection unavailable through ", + program, + ": ", + reason, + ], ""), + } + } +} + +fn provider_event_projection_readable( + observation: ProviderEventProjectionObservation, +) -> Bool { + match observation { + ProviderEventProjectionObserved { events: _ } => true + ProviderEventProjectionRefused { reason: _ } => false + } +} + +fn provider_event_projection_events( + observation: ProviderEventProjectionObservation, +) -> String { + match observation { + ProviderEventProjectionObserved { events } => events + ProviderEventProjectionRefused { reason: _ } => "" + } +} + +fn provider_event_projection_error( + observation: ProviderEventProjectionObservation, +) -> String { + match observation { + ProviderEventProjectionObserved { events: _ } => "" + ProviderEventProjectionRefused { reason } => reason + } } fn belt_workflow_progress_for_attempt_ref( @@ -1877,7 +1925,7 @@ fn belt_workflow_progress_for_attempt_ref( node_id: node_id, ), ) - let events = belt_provider_events_observe_for_instance( + let events = belt_provider_event_projection_observe_for_instance( instance: instance, node_id: node_id, ) @@ -1905,13 +1953,13 @@ fn belt_workflow_progress_for_attempt_ref( admission_receipt: admission.content, spawn_failure_present: spawn_failure.success, spawn_failure: spawn_failure.content, - provider_events_readable: direct_provider_events_observation_readable( + provider_events_readable: provider_event_projection_readable( observation: events, ), - provider_events: direct_provider_events_observation_content( + provider_events: provider_event_projection_events( observation: events, ), - provider_events_error: direct_provider_events_observation_error( + provider_events_error: provider_event_projection_error( observation: events, ), process: attempt_process_for_node( diff --git a/dag/gunbc/roadmap_dashboard_instance.dag b/dag/gunbc/roadmap_dashboard_instance.dag index 683ccf4dd7b..2ef669f676f 100644 --- a/dag/gunbc/roadmap_dashboard_instance.dag +++ b/dag/gunbc/roadmap_dashboard_instance.dag @@ -24,6 +24,7 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, + ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -257,6 +258,13 @@ fn dashboard_execution_environment( identity_contains: "coreutils" as NonEmptyStr, ensure_policy: EnsureObserveOnly, }, + CapabilityRealization { + capability: ProviderEventProjectionCapability, + executable: "/usr/bin/jq" as FilePath, + identity_args: ["--version"], + identity_contains: "jq-" as NonEmptyStr, + ensure_policy: EnsureObserveOnly, + }, CapabilityRealization { capability: EnvironmentBindingCapability, executable: dashboard_environment_binding_executable, diff --git a/dag/gunbc/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap_dispatch_actuator.dag index 1c17f52e880..242368fd15f 100644 --- a/dag/gunbc/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap_dispatch_actuator.dag @@ -110,6 +110,7 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, + ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -117,6 +118,9 @@ import gunbc.roadmap_execution_contract { execution_capability_label, execution_contract_validation_summary, } +import gunbc.roadmap_provider_events { + codex_provider_event_projection_args, +} import gunbc.repo_identity { gunbc_merge_target_ref } data dispatch_tmux_process_fingerprint_observation_dissolution_trigger: Disposition = Terminal { @@ -646,6 +650,24 @@ fn dispatch_current_attempt_events_path_for_instance( ], "") } +fn dispatch_current_attempt_events_projection_argv_for_instance( + instance: HostDashboardInstance, + node_id: RoadmapNodeId, +) -> HostExecArgv { + host_exec_argv( + program: dispatch_capability_program( + environment: instance.execution_environment, + cap: ProviderEventProjectionCapability, + ), + args: codex_provider_event_projection_args( + path: dispatch_current_attempt_events_path_for_instance( + instance: instance, + node_id: node_id, + ) as FilePath, + ), + ) +} + fn dispatch_branch_name(node_id: RoadmapNodeId, attempt_key: String) -> String { join(["dispatch/", node_id as String, "-a", attempt_key], "") } diff --git a/dag/gunbc/roadmap_execution_contract.dag b/dag/gunbc/roadmap_execution_contract.dag index 1b11ae92a19..5fa33035a57 100644 --- a/dag/gunbc/roadmap_execution_contract.dag +++ b/dag/gunbc/roadmap_execution_contract.dag @@ -8,6 +8,7 @@ type ExecutionCapability | TmuxSessionCapability | TmuxPipeShellCapability | TeeEventCaptureCapability + | ProviderEventProjectionCapability | EnvironmentBindingCapability | AttemptStateDirectoryCapability | AttemptCurrentPointerCapability @@ -57,6 +58,7 @@ fn execution_capability_label(capability: ExecutionCapability) -> String { TmuxSessionCapability => "tmux-session" TmuxPipeShellCapability => "tmux-pipe-shell" TeeEventCaptureCapability => "tee-event-capture" + ProviderEventProjectionCapability => "provider-event-projection" EnvironmentBindingCapability => "environment-binding" AttemptStateDirectoryCapability => "attempt-state-directory" AttemptCurrentPointerCapability => "attempt-current-pointer" @@ -163,6 +165,7 @@ fn dispatch_platform_required_capabilities() -> List { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, + ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, diff --git a/dag/gunbc/roadmap_provider_events.dag b/dag/gunbc/roadmap_provider_events.dag index 58e17f18a5e..6ec8db0b16e 100644 --- a/dag/gunbc/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap_provider_events.dag @@ -78,9 +78,40 @@ data codex_provider_projection_message_budget_note: String = "The agent's REPORT data codex_provider_projection_message_budget: CharacterCount = character_count(count: 8192) -data codex_jsonl_projection_note: String = "The attempt-owned provider-events.jsonl remains the complete Codex protocol authority. Workflow observation reads that file directly through gunbc.direct_process_observation.observe_provider_events_file and classifies the bytes in-process through parse_codex_jsonl_with_line_budget. Each physical line must be one JSON value and must fit within codex_provider_event_line_observation_budget; oversized command aggregated_output therefore refuses with a located diagnostic while the full raw remains in attempt evidence. Large command output is opaque to the synchronous dashboard without becoming lost evidence or an invented success." +data codex_provider_message_truncation_note: String = "TRUNCATION IS REPORTED, NEVER SILENT — the one place this projection could otherwise fabricate a plausible output (DESIGN §5). Everywhere else the bounds guard a machine-read classification, where a cut string simply fails to match and refuses; here the bounded value is PROSE AN OPERATOR READS AND ACTS ON, so a report severed mid-sentence and presented as if complete is a wrong answer delivered confidently — the operator would judge the work on a fragment while the screen gave no sign anything was missing. The projected envelope therefore carries the boundedness as its own member — a `message` object pairing the bounded text with a `truncated` flag — where truncated is computed from the ORIGINAL length before slicing, so it is a statement about the provider's actual output rather than about the slice. A consumer that renders text without consulting truncated is re-opening the arm this row closes, which is why the flag is a sibling of the text and not a derived guess about whether the slice happens to sit on the budget." -data codex_jsonl_classifier_note: String = "Codex --json is the provider protocol authority: one JSON object per line with thread.started, turn.started, item.*, turn.completed, turn.failed, and error event types. The shared RFC 8259 grammar proves each line parseable, then the classifier requires the emitted top-level `type` member to be the first object member before selecting a closed event variant. That fail-closed envelope rule prevents a nested item or user-authored string containing `type: turn.completed` from fabricating terminal success. Lines are mechanically bounded by codex_provider_event_line_observation_budget; the remaining line budget is a red control against drift and refuses before grammar descent with the payload elided. Unknown, reordered, malformed, or truncated lines still refuse with their located evidence and parsed prefix. A refusal is a workflow fact, never dropped as console noise. Item activity is only a presentation projection from the bounded item type; terminal success/failure comes only from the anchored top-level turn.completed, turn.failed, or error envelope, never from tmux Spawned." +data codex_provider_event_projection_filter: NonEmptyStr = join([ + "fromjson | def event_type: if (.type | type) == \"string\" then .type[0:", + to_string(character_count_value(c: codex_provider_projection_type_budget)), + "] else \"unknown\" end; def item_type: if (.item.type | type) == \"string\" then .item.type[0:", + to_string(character_count_value(c: codex_provider_projection_type_budget)), + "] else \"unknown\" end; def bounded_text: if . == null then \"\" elif type == \"string\" then .[0:", + to_string(character_count_value(c: codex_provider_projection_detail_budget)), + "] elif type == \"object\" and (.message | type) == \"string\" then .message[0:", + to_string(character_count_value(c: codex_provider_projection_detail_budget)), + "] else tostring[0:", + to_string(character_count_value(c: codex_provider_projection_detail_budget)), + "] end; def agent_report: if (.item.text | type) == \"string\" then {\"text\":(.item.text[0:", + 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 == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} else {\"type\":$type} end", +], "") as NonEmptyStr + +fn codex_provider_event_projection_args(path: FilePath) -> List { + [ + "--raw-input", + "--compact-output", + codex_provider_event_projection_filter as String, + path as String, + ] +} + +data codex_jsonl_projection_note: String = "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 envelope (top-level event type, bounded item type, and a bounded terminal error detail); two values on one line, a multiline value, and malformed JSON all refuse. Large command aggregated_output therefore becomes opaque to the synchronous dashboard without becoming lost evidence or an invented success. The exact jq executable and identity probe are part of dispatch-platform Ensure and dashboard provisioning; absence refuses before dispatch and is also 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_jsonl_classifier_note: String = "Codex --json is the provider protocol authority: one JSON object per line with thread.started, turn.started, item.*, turn.completed, turn.failed, and error event types. The raw stream is structurally reduced by codex_provider_event_projection_filter before this bounded total envelope classifier runs. The shared RFC 8259 grammar proves each projected line parseable, then the classifier requires the emitted top-level `type` member to be the first object member before selecting a closed event variant. That fail-closed envelope rule prevents a nested item or user-authored string containing `type: turn.completed` from fabricating terminal success. Projected lines are mechanically bounded; the remaining line budget is a red control against projector drift and refuses before grammar descent with the payload elided. Unknown, reordered, malformed, or truncated projected lines still refuse with their located evidence and parsed prefix. A refusal is a workflow fact, never dropped as console noise. Item activity is only a presentation projection from the bounded item type; terminal success/failure comes only from the anchored top-level turn.completed, turn.failed, or error envelope, never from tmux Spawned." data codex_projected_envelope_walk_note: String = "review 44058. Both reads below are ANCHORED walks over the projector's own envelope shape, not scans. codex_line_contains_type_token, which this replaces, searched the whole line for a `\"type\":\"…\"` fragment in either spacing — so an agent_message whose text quoted `\"type\":\"file_change\"` reclassified the pane's activity, the same fragment-scan class the top-level classifier already refused to use for terminal events and the same one the dashboard health readback was repaired for. The projector emits exactly {\"type\":…} for plain events and {\"type\":…,\"item\":{\"type\":…}} for item events — plus, on an agent_message item only, a \"message\" member SIBLING to and AFTER item.type carrying the bounded report and its truncation flag. The anchor is unaffected by that addition and that is why it was placed there: item.type remains item's FIRST member, so the walk's landing position is unchanged, and a report whose own text quotes an item-type fragment still cannot reclassify the pane because the walk never reaches it. Both anchored members have a known position and the walk either lands on it or answers false. codex_envelope_member_value_start is the single step both readings share (DESIGN 2, one concept)." diff --git a/dag/gunbc/roadmap_serve.dag b/dag/gunbc/roadmap_serve.dag index 523b35c23c5..97805976e74 100644 --- a/dag/gunbc/roadmap_serve.dag +++ b/dag/gunbc/roadmap_serve.dag @@ -412,7 +412,7 @@ fn serve_attempts_response() -> ServeHttpResponse { serve_attempts_response_for_instance(instance: srv1_live_dashboard_instance()) } -data serve_workflow_note: String = "GET /workflow.json is the evidence-derived attempt pipeline. One observation joins current modeled state, a direct bounded read of complete Codex attempt evidence, retained process exit, and the node's declared validation contract. Provider events are read in-process through gunbc.direct_process_observation and classified through parse_codex_jsonl; malformed input or an unreadable file remains an explicit per-attempt refusal. The observed facts reconcile by fold against the shared desired obligation roster and expose desired/observed/remaining counts plus distinct observed-failure, observed-refusal, and unsafe-plan-refusal counts; they never upgrade provider completion into verification, publication, review, or goal audit. A refused attempts ledger returns 503; per-attempt event/process refusals remain explicit segment states in a 200 body so one damaged attempt cannot erase evidence from every other row." +data serve_workflow_note: String = "GET /workflow.json is the evidence-derived attempt pipeline. One observation joins current modeled state, a bounded jq projection of complete Codex attempt evidence, retained process exit, and the node's declared validation contract. The exact projector executable is an ensured platform capability. It validates each raw JSON value while omitting unbounded command payloads from the synchronous interpreted handler; projector absence or malformed input remains an explicit per-attempt refusal. The observed facts reconcile by fold against the shared desired obligation roster and expose desired/observed/remaining counts plus distinct observed-failure, observed-refusal, and unsafe-plan-refusal counts; they never upgrade provider completion into verification, publication, review, or goal audit. A refused attempts ledger returns 503; per-attempt event/process refusals remain explicit segment states in a 200 body so one damaged attempt cannot erase evidence from every other row." fn serve_workflow_response_for_instance( instance: HostDashboardInstance, diff --git a/dag/gunbc/roadmap_workflow_progress.dag b/dag/gunbc/roadmap_workflow_progress.dag index 796ac661a3e..ef8d71433dd 100644 --- a/dag/gunbc/roadmap_workflow_progress.dag +++ b/dag/gunbc/roadmap_workflow_progress.dag @@ -111,7 +111,7 @@ type WorkflowAttemptProgress { reconciliation: WorkflowReconcileReport } -data workflow_progress_projection_note: String = "The row progress bar is a pure projection over independent evidence, never a stored workflow status. An attempt state pointer is published only after environment admission, worktree creation, an immutable admission receipt, and an empty provider-events file all exist. Readability alone is not admission: the projection requires the receipt to START with the emitter's own admitted identity prefix (schema then status), so a corrupted or refused receipt cannot paint Environment complete. Positional identity replaces a text scan that any value position could satisfy. Complete Codex JSONL remains attempt evidence; gunbc.direct_process_observation reads provider-events.jsonl directly and parse_codex_jsonl enforces the CharacterCount line budget before this module derives agent activity and provider terminal events. Large command output is therefore opaque to the synchronous UI rather than a workflow refusal. The shared gunbc.worker_lifecycle.WorkerProcessEvidence supplies process presence and exit to both the Sessions surface and Agent reconciliation. Reconciliation is fail-closed, and Agent completes ONLY on the design contract's compatible process exit: a failed event projection refuses, nonzero exit before a terminal event is failure, zero exit without a terminal event is refused/incomplete, malformed events remain refused, contradictory turn.completed plus nonzero process exit is refused, turn.completed with the pane still running stays ACTIVE awaiting that exit, and turn.completed with an absent or unobserved process REFUSES because no exit was established. A terminal provider event alone never paints Agent complete — that would fabricate a completed obligation from incomplete evidence. The ordered desired facts derive from roadmap_attempt_workflow_segment_kinds; only non-pending evidence projections enter the observed fact forest, so Pending in the row means an absent observation rather than a fabricated pending receipt. The two forests reconcile through gunbc.workflow_reconcile, making observed count, remaining work, and overall convergence fold results rather than another hand-authored status. Verification, publication, review, and goal audit remain pending until their own authoritative receipts exist; provider completion cannot fabricate them." +data workflow_progress_projection_note: String = "The row progress bar is a pure projection over independent evidence, never a stored workflow status. An attempt state pointer is published only after environment admission, worktree creation, an immutable admission receipt, and an empty provider-events file all exist. Readability alone is not admission: the projection requires the receipt to START with the emitter's own admitted identity prefix (schema then status), so a corrupted or refused receipt cannot paint Environment complete. Positional identity replaces a text scan that any value position could satisfy. Complete Codex JSONL remains attempt evidence; the observer's modeled jq capability validates it and emits bounded structural envelopes before this module derives agent activity and provider terminal events. Large command output is therefore opaque to the synchronous UI rather than a workflow refusal. The shared gunbc.worker_lifecycle.WorkerProcessEvidence supplies process presence and exit to both the Sessions surface and Agent reconciliation. Reconciliation is fail-closed, and Agent completes ONLY on the design contract's compatible process exit: a failed event projection refuses, nonzero exit before a terminal event is failure, zero exit without a terminal event is refused/incomplete, malformed events remain refused, contradictory turn.completed plus nonzero process exit is refused, turn.completed with the pane still running stays ACTIVE awaiting that exit, and turn.completed with an absent or unobserved process REFUSES because no exit was established. A terminal provider event alone never paints Agent complete — that would fabricate a completed obligation from incomplete evidence. The ordered desired facts derive from roadmap_attempt_workflow_segment_kinds; only non-pending evidence projections enter the observed fact forest, so Pending in the row means an absent observation rather than a fabricated pending receipt. The two forests reconcile through gunbc.workflow_reconcile, making observed count, remaining work, and overall convergence fold results rather than another hand-authored status. Verification, publication, review, and goal audit remain pending until their own authoritative receipts exist; provider completion cannot fabricate them." diff --git a/dag/test/claim/direct_process_observation_witness_test.dag b/dag/test/claim/direct_process_observation_witness_test.dag index 85a0937af7c..5637221e4eb 100644 --- a/dag/test/claim/direct_process_observation_witness_test.dag +++ b/dag/test/claim/direct_process_observation_witness_test.dag @@ -1,37 +1,117 @@ module test.claim.direct_process_observation_witness_test -import std.types { Bool } +import std.types { Bool, String, Int, Bytes } +import std.bytes { utf8_encode_bytes } import gunbc.direct_process_observation { - DirectProviderEventsObserved, - DirectProviderEventsRefused, - direct_provider_events_observation_readable, - direct_provider_events_observation_content, - direct_provider_events_observation_error, + DirectProcessObservation, + ProcessCompleted, + ProcessSpawnRefused, + ProcessWaitRefused, + ProcessExited, + ProcessSignaled, + ProcessTimedOut, + ProcessKilled, + direct_process_observation_from_shell, + direct_process_observation_completed_p, } -data direct_process_observation_witness_note: String = "The direct-read substrate is pure projection over Filesystem.Read semantics: success exposes content, failure carries a located reason and never fabricates an empty stream. These witnesses pin the three accessors the belt consumes so a refusal cannot be misread as readable-with-empty." +data witness_note: String = "Hermetic witnesses for gunbc.direct_process_observation: spawn/wait refusal, termination variants, and the absence of a precomputed success Bool." -test fn observed_content_is_readable_and_round_trips() -> Bool { - let observation = DirectProviderEventsObserved { content: "{\"type\":\"turn.started\"}\n" } - direct_provider_events_observation_readable(observation: observation) - && direct_provider_events_observation_content(observation: observation) - == "{\"type\":\"turn.started\"}\n" - && direct_provider_events_observation_error(observation: observation) == "" +fn spawn_refusal_is_typed() -> Bool { + match direct_process_observation_from_shell( + spawn_ok: false, + wait_ok: true, + timed_out: false, + exit_code: 0, + signaled: false, + signal: 0, + exit_code_present: true, + stdout: utf8_encode_bytes(s: ""), + stderr: utf8_encode_bytes(s: ""), + spawn_detail: "spawn refused", + wait_detail: "", + ) { + ProcessSpawnRefused { detail } => (detail as String) == "spawn refused" + ProcessCompleted { termination: _, streams: _ } => false + ProcessWaitRefused { detail: _ } => false + } +} + +fn wait_refusal_is_typed() -> Bool { + match direct_process_observation_from_shell( + spawn_ok: true, + wait_ok: false, + timed_out: false, + exit_code: 0, + signaled: false, + signal: 0, + exit_code_present: true, + stdout: utf8_encode_bytes(s: ""), + stderr: utf8_encode_bytes(s: ""), + spawn_detail: "", + wait_detail: "wait refused", + ) { + ProcessWaitRefused { detail } => (detail as String) == "wait refused" + ProcessCompleted { termination: _, streams: _ } => false + ProcessSpawnRefused { detail: _ } => false + } +} + +fn exited_termination_carries_code_without_sentinel() -> Bool { + match direct_process_observation_from_shell( + spawn_ok: true, + wait_ok: true, + timed_out: false, + exit_code: 17, + signaled: false, + signal: 0, + exit_code_present: true, + stdout: utf8_encode_bytes(s: "ok"), + stderr: utf8_encode_bytes(s: ""), + spawn_detail: "", + wait_detail: "", + ) { + ProcessCompleted { termination, streams: _ } => + match termination { + ProcessExited { code } => code == 17 + ProcessSignaled { signal: _ } => false + ProcessTimedOut => false + ProcessKilled => false + } + ProcessSpawnRefused { detail: _ } => false + ProcessWaitRefused { detail: _ } => false + } } -test fn refused_observation_is_not_readable_and_carries_reason() -> Bool { - let observation = DirectProviderEventsRefused { - reason: "provider event stream is unreadable at /missing: no such file" as NonEmptyStr, +fn absent_exit_code_becomes_killed_not_minus_one() -> Bool { + match direct_process_observation_from_shell( + spawn_ok: true, + wait_ok: true, + timed_out: false, + exit_code: 0, + signaled: false, + signal: 0, + exit_code_present: false, + stdout: utf8_encode_bytes(s: ""), + stderr: utf8_encode_bytes(s: ""), + spawn_detail: "", + wait_detail: "", + ) { + ProcessCompleted { termination, streams: _ } => + match termination { + ProcessKilled => true + ProcessExited { code: _ } => false + ProcessSignaled { signal: _ } => false + ProcessTimedOut => false + } + ProcessSpawnRefused { detail: _ } => false + ProcessWaitRefused { detail: _ } => false } - !direct_provider_events_observation_readable(observation: observation) - && direct_provider_events_observation_content(observation: observation) == "" - && string_contains( - s: direct_provider_events_observation_error(observation: observation), - pattern: "/missing", - ) } test fn direct_process_observation_keystone_holds() -> Bool { - observed_content_is_readable_and_round_trips() - && refused_observation_is_not_readable_and_carries_reason() + spawn_refusal_is_typed() + && wait_refusal_is_typed() + && exited_termination_carries_code_without_sentinel() + && absent_exit_code_becomes_killed_not_minus_one() } diff --git a/dag/test/claim/git_ls_remote_advertisement_witness_test.dag b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag new file mode 100644 index 00000000000..08501f75676 --- /dev/null +++ b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag @@ -0,0 +1,112 @@ +module test.claim.git_ls_remote_advertisement_witness_test + +import std.types { Bool, String, Int, List, NonEmptyStr, Bytes } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.git.ls_remote_advertisement { + GitAdvertisedRefDecodeOutcome, + GitAdvertisedRefsDecoded, + GitAdvertisedRefLineMalformed, + GitAdvertisedRefObjectIdRefused, + GitAdvertisedRefDuplicate, + GitAdvertisedRefEmptyBranchProjection, + git_decode_ls_remote_stdout, + git_decode_advertised_ref_line, + advertised_ref_branch_name, +} +import extdeps.git.object_store { + GitObjectFormatSha1, + GitObjectIdTextNonHexadecimal, + GitObjectIdTextNonCanonicalUppercase, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data fixture_sha_a: String = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +data fixture_sha_b: String = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +fn tab() -> String { from_code_point(cp: 9) } + +fn line_of(sha: String, ref_name: String) -> String { + concat(sha, concat(tab(), ref_name)) +} + +fn branch_name_projects_from_full_ref() -> Bool { + advertised_ref_branch_name(ref_name: "refs/heads/main") == "main" + && advertised_ref_branch_name(ref_name: "refs/heads/dispatch/node-a7") == "dispatch/node-a7" +} + +fn malformed_line_refuses_at_index() -> Bool { + match git_decode_ls_remote_stdout( + stdout: concat( + line_of(sha: fixture_sha_a, ref_name: "refs/heads/main"), + concat("\n", "no-tab-line"), + ), + format: GitObjectFormatSha1, + ) { + GitAdvertisedRefLineMalformed { line_index, line } => + line_index == 1 && line == "no-tab-line" + GitAdvertisedRefsDecoded { refs: _ } => false + GitAdvertisedRefObjectIdRefused { line_index: _, line: _, cause: _ } => false + GitAdvertisedRefDuplicate { ref_name: _, first_index: _, second_index: _ } => false + GitAdvertisedRefEmptyBranchProjection { line_index: _, ref_name: _ } => false + } +} + +fn invalid_object_id_refuses_not_brands() -> Bool { + match git_decode_advertised_ref_line( + line: line_of(sha: "not-hex", ref_name: "refs/heads/main"), + line_index: 0, + format: GitObjectFormatSha1, + ) { + GitAdvertisedRefObjectIdRefused { cause, line_index: _, line: _ } => + match cause { + GitObjectIdTextNonHexadecimal => true + _ => false + } + GitAdvertisedRefLineDecoded { row: _ } => false + GitAdvertisedRefLineMalformed { line_index: _, line: _ } => false + GitAdvertisedRefEmptyBranchProjection { line_index: _, ref_name: _ } => false + } +} + +fn duplicate_ref_name_refuses_instead_of_first_match() -> Bool { + match git_decode_ls_remote_stdout( + stdout: concat( + concat( + line_of(sha: fixture_sha_a, ref_name: "refs/heads/main"), + "\n", + ), + line_of(sha: fixture_sha_b, ref_name: "refs/heads/main"), + ), + format: GitObjectFormatSha1, + ) { + GitAdvertisedRefDuplicate { ref_name, first_index, second_index } => + ref_name == "refs/heads/main" && first_index == 0 && second_index == 1 + GitAdvertisedRefsDecoded { refs: _ } => false + GitAdvertisedRefLineMalformed { line_index: _, line: _ } => false + GitAdvertisedRefObjectIdRefused { line_index: _, line: _, cause: _ } => false + GitAdvertisedRefEmptyBranchProjection { line_index: _, ref_name: _ } => false + } +} + +fn empty_heads_prefix_projection_refuses() -> Bool { + match git_decode_ls_remote_stdout( + stdout: line_of(sha: fixture_sha_a, ref_name: "refs/heads/"), + format: GitObjectFormatSha1, + ) { + GitAdvertisedRefEmptyBranchProjection { line_index, ref_name } => + line_index == 0 && ref_name == "refs/heads/" + GitAdvertisedRefsDecoded { refs: _ } => false + GitAdvertisedRefLineMalformed { line_index: _, line: _ } => false + GitAdvertisedRefObjectIdRefused { line_index: _, line: _, cause: _ } => false + GitAdvertisedRefDuplicate { ref_name: _, first_index: _, second_index: _ } => false + } +} + +test fn git_ls_remote_advertisement_keystone_holds() -> Bool { + branch_name_projects_from_full_ref() + && malformed_line_refuses_at_index() + && invalid_object_id_refuses_not_brands() + && duplicate_ref_name_refuses_instead_of_first_match() + && empty_heads_prefix_projection_refuses() +} diff --git a/dag/test/claim/repository_remote_head_real_execution_witness_test.dag b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag new file mode 100644 index 00000000000..2ccd44ccef2 --- /dev/null +++ b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag @@ -0,0 +1,92 @@ +module test.claim.repository_remote_head_real_execution_witness_test + +import std.types { Bool, String, NonEmptyStr, FilePath, Bytes } +import extdeps.shell +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.repository_remote_head { + GitRepositoryBinding, + GitForgeIdentity, + GitCredentialUnauthenticated, + RepositoryRemoteHeadObservation, + RepositoryRemoteHeadsRead, + RepositoryRemoteHeadProcessRefused, + RepositoryRemoteHeadDecodeRefused, + observe_repository_remote_heads, + repository_remote_head_branch_sha, + repository_remote_head_invocation, +} +import v2.std.operation_argv { + ArgvMaterialized, + ArgvMaterializationRefused, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data real_execution_home_doc: String = "Wet execution witnesses for repository-bound remote-head observation against a temporary local bare repository. No network, no credentials, no live GitHub. Excluded from hermetic discovery and enrolled in bin_witness_wet_entries." + +fn witness_binding(remote_url: String, worktree: String) -> GitRepositoryBinding { + GitRepositoryBinding { + worktree: worktree as FilePath, + remote_url: remote_url as NonEmptyStr, + forge: GitForgeIdentity { remote_name: "origin" as NonEmptyStr }, + credential: GitCredentialUnauthenticated, + } +} + +fn run(script: String) -> Bool { + shell.Exec.Run(script: script as extdeps.shell.exec.TransportScript).success +} + +test fn repository_remote_head_bare_repo_by_real_execution() -> Bool { + let root = shell.Mktemp.Dir() + let bare = concat(root.path as String, "/remote.git") + let work = concat(root.path as String, "/work") + let setup = + run(script: concat("git init --bare ", bare)) + && run(script: concat("git init ", work)) + && run(script: concat("cd ", work, " && git config user.email witness@test && git config user.name witness")) + && run(script: concat("cd ", work, " && git remote add origin ", bare)) + && run(script: concat("cd ", work, " && echo file > f && git add f && git commit -m init && git push -u origin main")) + let binding = witness_binding(remote_url: bare, worktree: work) + let inv = repository_remote_head_invocation(binding: binding) + let materialized = shell_materialize_operation_argv( + path: inv.at.path, + service: inv.at.service, + operation: inv.at.operation, + bindings: inv.bindings, + ) + let observed = observe_repository_remote_heads(binding: binding) + let moved = + run(script: concat("cd ", work, " && echo file2 >> f && git add f && git commit -m move && git push")) + && observe_repository_remote_heads(binding: binding) + let unavailable = observe_repository_remote_heads( + binding: witness_binding(remote_url: concat(bare, "-missing"), worktree: work), + ) + let cleaned = shell.Remove.RecursiveForce(path: root.path) + match materialized { + ArgvMaterializationRefused { at: _, cause: _ } => false + ArgvMaterialized { argv } => + count(argv) >= 3 && argv.first() == "git" + } + && setup + && match observed { + RepositoryRemoteHeadsRead { heads } => count(heads) == 1 + RepositoryRemoteHeadProcessRefused { observation: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + && match moved { + RepositoryRemoteHeadsRead { heads } => count(heads) == 1 + RepositoryRemoteHeadProcessRefused { observation: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + && match unavailable { + RepositoryRemoteHeadProcessRefused { observation: _ } => true + RepositoryRemoteHeadsRead { heads: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + && cleaned.success +} + +test fn repository_remote_head_keystone_holds_by_real_execution() -> Bool { + repository_remote_head_bare_repo_by_real_execution() +} diff --git a/dag/test/claim/roadmap_dispatch_environment_witness_test.dag b/dag/test/claim/roadmap_dispatch_environment_witness_test.dag index ee71de37b54..fdc7da8118d 100644 --- a/dag/test/claim/roadmap_dispatch_environment_witness_test.dag +++ b/dag/test/claim/roadmap_dispatch_environment_witness_test.dag @@ -12,6 +12,7 @@ import gunbc.roadmap_execution_contract { TmuxSessionCapability, TmuxPipeShellCapability, TeeEventCaptureCapability, + ProviderEventProjectionCapability, EnvironmentBindingCapability, AttemptStateDirectoryCapability, AttemptCurrentPointerCapability, @@ -100,6 +101,7 @@ fn platform_grounded_receipts() -> List { grounded_capability(cap: TmuxSessionCapability), grounded_capability(cap: TmuxPipeShellCapability), grounded_capability(cap: TeeEventCaptureCapability), + grounded_capability(cap: ProviderEventProjectionCapability), grounded_capability(cap: EnvironmentBindingCapability), grounded_capability(cap: AttemptStateDirectoryCapability), grounded_capability(cap: AttemptCurrentPointerCapability), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 4533c72facb..f94be496f36 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5711,9 +5711,24 @@ fn build_service_param_env( } struct ShellResult { - exit_code: i32, - stdout: String, - stderr: String, + exit_code: Option, + signaled: bool, + signal: i32, + stdout_raw: Vec, + stderr_raw: Vec, +} + +fn bytes_to_value(bytes: &[u8]) -> Value { + list_value( + bytes + .iter() + .map(|b| Value::Int(i64::from(*b))) + .collect::>(), + ) +} + +fn shell_stdout_string_lossy(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).into_owned() } fn push_shell_argv_tokens(argv: &mut Vec, val: Value) -> InterpResult<()> { @@ -6849,6 +6864,8 @@ fn dispatch_shell( intent: &str, expected: ExpectedOutcome, ) -> InterpResult { + #[cfg(unix)] + use std::os::unix::process::ExitStatusExt; // `expected` is the caller's DECLARED expectation, threaded from the sibling edge // on the call node (EFFECT_EXPECTATION_ARG). Absent at the call site it is // ExpectSuccess, which keeps every undeclared site behaviour-identical to the @@ -6953,16 +6970,25 @@ fn dispatch_shell( output }; - let exit_code = output.status.code().unwrap_or(-1); + let exit_code = output.status.code(); + #[cfg(unix)] + let signaled = output + .status + .signal() + .is_some_and(|_| !output.status.success() && exit_code.is_none()); + #[cfg(not(unix))] + let signaled = false; + #[cfg(unix)] + let signal = output.status.signal().unwrap_or(0) as i32; + #[cfg(not(unix))] + let signal = 0; Ok(ShellResult { exit_code, - stdout: String::from_utf8_lossy(&output.stdout) - .trim_end() - .to_string(), - stderr: String::from_utf8_lossy(&output.stderr) - .trim_end() - .to_string(), + signaled, + signal, + stdout_raw: output.stdout, + stderr_raw: output.stderr, }) } @@ -6974,7 +7000,7 @@ fn map_shell_outputs( let return_type = match op_node.inferred.as_deref() { Some(crate::v1_std_core::InferredNode::Resolved { node }) => node.clone(), _ => { - return Ok(Value::Str(result.stdout.clone())); + return Ok(Value::Str(shell_stdout_string_lossy(&result.stdout_raw))); } }; @@ -6988,27 +7014,40 @@ fn map_shell_outputs( let field_name = authored_name_at(ctx.si(), child.clone()); let from_key = extract_from_key(child, ctx); let is_optional_field = child.return_cardinality == Cardinality::CardOptional; + let exit_code_i64 = result.exit_code.map(i64::from).unwrap_or(0); + let process_failed = result.signaled || result.exit_code != Some(0); + let stdout_str = shell_stdout_string_lossy(&result.stdout_raw); + let stderr_str = shell_stdout_string_lossy(&result.stderr_raw); let value = match from_key.as_deref() { - Some("stdout") if is_optional_field && result.exit_code != 0 => Value::Null, - Some("stderr") if is_optional_field && result.exit_code != 0 => Value::Null, - Some("stdout") => Value::Str(result.stdout.clone()), - Some("stderr") => Value::Str(result.stderr.clone()), - Some("exit_success") => Value::Bool(result.exit_code == 0), - Some("exit_code") => Value::Int(result.exit_code as i64), + Some("stdout") if is_optional_field && process_failed => Value::Null, + Some("stderr") if is_optional_field && process_failed => Value::Null, + Some("stdout") => Value::Str(stdout_str.clone()), + Some("stderr") => Value::Str(stderr_str.clone()), + Some("stdout_bytes") => bytes_to_value(&result.stdout_raw), + Some("stderr_bytes") => bytes_to_value(&result.stderr_raw), + Some("exit_success") => Value::Bool(result.exit_code == Some(0)), + Some("exit_code") => Value::Int(exit_code_i64), + Some("exit_code_present") => Value::Bool(result.exit_code.is_some()), + Some("signaled") => Value::Bool(result.signaled), + Some("signal") => Value::Int(result.signal as i64), Some("stdout_lines") => { - let lines: Vec = result - .stdout + let lines: Vec = stdout_str .lines() .map(|l| Value::Str(l.to_string())) .collect(); list_value((lines)) } _ => match field_name.as_str() { - "success" => Value::Bool(result.exit_code == 0), - "exit_code" => Value::Int(result.exit_code as i64), - "stdout" => Value::Str(result.stdout.clone()), - "stderr" => Value::Str(result.stderr.clone()), - "exists" => Value::Bool(result.exit_code == 0), + "success" => Value::Bool(result.exit_code == Some(0)), + "exit_code" => Value::Int(exit_code_i64), + "exit_code_present" => Value::Bool(result.exit_code.is_some()), + "signaled" => Value::Bool(result.signaled), + "signal" => Value::Int(result.signal as i64), + "stdout" => Value::Str(stdout_str.clone()), + "stderr" => Value::Str(stderr_str.clone()), + "stdout_bytes" => bytes_to_value(&result.stdout_raw), + "stderr_bytes" => bytes_to_value(&result.stderr_raw), + "exists" => Value::Bool(result.exit_code == Some(0)), _ => Value::Null, }, }; From f516a12cb3ce037a81a9995ae182bac53566b1fe Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:13:04 +0000 Subject: [PATCH 03/10] WIP: Direct-process observation substrate --- dag/extdeps/git/git.dag | 3 +-- dag/gunbc/direct_process_observation.dag | 2 +- dag/gunbc/roadmap_publish_observe.dag | 6 ++---- dag/test/claim/roadmap_publish_observe_witness_test.dag | 2 -- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/dag/extdeps/git/git.dag b/dag/extdeps/git/git.dag index c6f99ffd8db..1a701db1612 100644 --- a/dag/extdeps/git/git.dag +++ b/dag/extdeps/git/git.dag @@ -272,7 +272,7 @@ type AdvertisedRef { data advertised_ref_scan_note: String = "The scan REFUSES on a line it cannot read rather than skipping it, and the located line is carried so the refusal names what it choked on. Dropping unparseable lines is the shape DESIGN 5 calls the absorbing fallback: the answer would still be a well-formed ref list, so a consumer could not tell a complete advertisement from a partial one, and the deficit's frequency would be zero by construction. A partial ref list is exactly the input that makes an exact-head judgment answer `not published` about a branch that is in fact published." -data ls_remote_carries_no_exit_block_note: String = "WHY LsRemoteHeads DECLARES NO `exit` BLOCK WHILE EVERY SIBLING OPERATION DOES, which is the first thing a reader will notice. The `exit` arms are INERT AT RUN TIME - established by reading the seed, not assumed. The shell dispatch in v1_interpreter.rs maps declared output sources (`stdout`, `stderr`, `exit_success`, `exit_code`, `stdout_lines`) onto the process result and returns a record; it never consults the operation's exit arms and never raises on a nonzero exit. The only consumer of exit_props anywhere outside the parser is v1_compiler_emit_rust.rs, which round-trips them back to source so the self-host fixed point holds. So an exit arm is a declaration that is parsed, re-emitted, and never realized.\\n\\nThat makes this operation's OUTPUTS the realized authority for its outcome: a caller learns whether the read succeeded from `success`/`exit_code`/`stderr`, which are values it can match on. Adding an exit block beside them would be a second representation of the same fact where only one of the two executes - and since this operation is new, authoring one knowing it is inert would be minting a fresh declared-but-unrealized instance rather than inheriting an existing one. The corpus-wide inertness of exit blocks is a real finding and a separate correction; it is not fixed by making one operation diverge from a hundred siblings, and it is not made worse by declining to add the hundred-and-first." +data ls_remote_carries_no_exit_block_note: String = "WHY LsRemoteHeads DECLARES NO `exit` BLOCK WHILE EVERY SIBLING OPERATION DOES, which is the first thing a reader will notice. The `exit` arms are INERT AT RUN TIME - established by reading the seed, not assumed. The shell dispatch in v1_interpreter.rs maps declared output sources onto the process result and returns a record; it never consults the operation's exit arms and never raises on a nonzero exit. The only consumer of exit_props anywhere outside the parser is v1_compiler_emit_rust.rs, which round-trips them back to source so the self-host fixed point holds. So an exit arm is a declaration that is parsed, re-emitted, and never realized.\\n\\nThat makes this operation's OUTPUTS the realized authority for its outcome. This operation deliberately omits `success: Bool from exit_success` (PR #7527 census: 148 such blocks in dag/extdeps discard the exit code beside an inert exit table; LsRemoteHeads is the Lane C exemplar fix, leaving 147). Callers read `exit_code` and `exit_code_present` directly; `exit_success` is a projection that belongs in the renderer, not the declaration." type AdvertisedRefScan = AdvertisedRefsRead { refs: List } @@ -584,7 +584,6 @@ service git.Core { input { remote: String } output { advertised: List from "stdout_lines" - success: Bool from "exit_success" exit_code: Int from "exit_code" stderr: String from "stderr" stdout: Bytes from "stdout_bytes" diff --git a/dag/gunbc/direct_process_observation.dag b/dag/gunbc/direct_process_observation.dag index e4a512d6387..d25aaddd779 100644 --- a/dag/gunbc/direct_process_observation.dag +++ b/dag/gunbc/direct_process_observation.dag @@ -3,7 +3,7 @@ module gunbc.direct_process_observation import std.types { String, Int, NonEmptyStr, Bytes } import std.bytes { utf8_encode_bytes } -data direct_process_observation_note: String = "Minimum process observation shape for LocalProcess execution (v2.std.execution_mechanism.ExecutionMechanism.LocalProcess — classification lands in PR #7526; this module is the observation carrier, not the mechanism classifier). Completed versus spawn-refused versus wait-refused. Exited versus signaled versus timed-out versus killed. No precomputed success Bool. No minus-one sentinel for a missing exit code. Streams are retained as Bytes at this boundary; the operation names its decoder. Consumed first by gunbc.repository_remote_head." +data direct_process_observation_note: String = "Minimum process observation shape for LocalProcess execution (v2.std.execution_mechanism.ExecutionMechanism.LocalProcess — classification lands in PR #7526; this module is the observation carrier, not the mechanism classifier). Completed versus spawn-refused versus wait-refused. Exited versus signaled versus timed-out versus killed. No precomputed success Bool. No minus-one sentinel for a missing exit code. Streams are retained as Bytes at this boundary; the operation names its decoder. Consumed first by gunbc.repository_remote_head. PR #7527 census (dag/extdeps): 148 operations declared exit_success beside an inert exit table; git.Core.LsRemoteHeads is the Lane C exemplar fix (147 remain)." type ProcessStreamBytes { stdout: Bytes diff --git a/dag/gunbc/roadmap_publish_observe.dag b/dag/gunbc/roadmap_publish_observe.dag index efd2208011a..65f49e269f7 100644 --- a/dag/gunbc/roadmap_publish_observe.dag +++ b/dag/gunbc/roadmap_publish_observe.dag @@ -15,7 +15,7 @@ import gunbc.roadmap_publish { data observer_role_note: String = "THIS IS THE HALF THAT TOUCHES THE NETWORK, SEPARATED FROM THE HALF THAT JUDGES. gunbc.roadmap_publish adjudicates observations a caller already holds, which is what makes every one of its arms reachable from constructed values in a witness. That property is worth keeping, so the reading lives here instead: this module calls the transport, decides what was learned, and hands back an ordinary value. The split is the reason the refusal arms are constructible at all - a function that called the service itself could not build them, because a transport failure there is not a value.\\n\\nWhat this module must never become is a second adjudicator. It reports what it saw and refuses when it could not see; which outcome that implies is roadmap_publish's business." -data remote_branch_observation_note: String = "THE REFUSAL IS BUILT FROM A VALUE, NOT CAUGHT. git.Core.LsRemoteHeads declares success/exit_code/stderr as outputs, so a failed ref advertisement arrives as an ordinary record this function can match on. That is not true of every transport in the corpus - the REST dispatch raises on a non-2xx rather than returning one - and the difference is exactly why the remote-branch half of publication can be observed today and the pull-request half cannot." +data remote_branch_observation_note: String = "THE REFUSAL IS BUILT FROM A VALUE, NOT CAUGHT. git.Core.LsRemoteHeads declares exit_code/stderr/advertised as outputs (no exit_success projection — PR #7527 census), so a failed ref advertisement arrives as an ordinary record this function can match on. That is not true of every transport in the corpus - the REST dispatch raises on a non-2xx rather than returning one - and the difference is exactly why the remote-branch half of publication can be observed today and the pull-request half cannot." fn ls_remote_refusal_detail(remote: String, exit_code: Int, stderr: String) -> String { concat( @@ -45,12 +45,11 @@ data transport_shim_split_note: String = "THE READING IS PURE; ONLY THE CALL IS fn remote_branch_observation_of( remote: NonEmptyStr, - success: Bool, exit_code: Int, stderr: String, advertised: List, ) -> RemoteBranchObservation { - if !success { + if exit_code != 0 { RemoteBranchesUnreadable { detail: ls_remote_refusal_detail( remote: remote as String, @@ -91,7 +90,6 @@ fn observe_remote_branches(remote: NonEmptyStr) -> RemoteBranchObservation { let result = git.Core.LsRemoteHeads(remote: remote as String) remote_branch_observation_of( remote: remote, - success: result.success, exit_code: result.exit_code, stderr: result.stderr, advertised: result.advertised, diff --git a/dag/test/claim/roadmap_publish_observe_witness_test.dag b/dag/test/claim/roadmap_publish_observe_witness_test.dag index d4b7d2f0558..ebb1e47ba0b 100644 --- a/dag/test/claim/roadmap_publish_observe_witness_test.dag +++ b/dag/test/claim/roadmap_publish_observe_witness_test.dag @@ -36,7 +36,6 @@ fn origin() -> NonEmptyStr { "origin" as NonEmptyStr } fn read_ok(advertised: List) -> RemoteBranchObservation { remote_branch_observation_of( remote: origin(), - success: true, exit_code: 0, stderr: "", advertised: advertised, @@ -75,7 +74,6 @@ data transport_refusal_note: String = "The claim that motivates the whole slice. test fn a_failed_ref_advertisement_refuses_rather_than_reading_empty() -> Bool { let o = remote_branch_observation_of( remote: origin(), - success: false, exit_code: 128, stderr: "fatal: repository not found", advertised: [], From 8594f3e709f272c1f1683c89f7ffb38b2958c238 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:31:15 +0000 Subject: [PATCH 04/10] Fix parse error in repository remote-head wet witness. The test chained && after a closing match brace, which the parser rejects as "expected expression, found Newline". Restructure with let bindings for each match result, matching other real_execution witnesses. Co-authored-by: Cursor --- ...emote_head_real_execution_witness_test.dag | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/dag/test/claim/repository_remote_head_real_execution_witness_test.dag b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag index 2ccd44ccef2..523a0e67515 100644 --- a/dag/test/claim/repository_remote_head_real_execution_witness_test.dag +++ b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag @@ -56,35 +56,33 @@ test fn repository_remote_head_bare_repo_by_real_execution() -> Bool { bindings: inv.bindings, ) let observed = observe_repository_remote_heads(binding: binding) - let moved = - run(script: concat("cd ", work, " && echo file2 >> f && git add f && git commit -m move && git push")) - && observe_repository_remote_heads(binding: binding) + let pushed = run(script: concat("cd ", work, " && echo file2 >> f && git add f && git commit -m move && git push")) + let moved = observe_repository_remote_heads(binding: binding) let unavailable = observe_repository_remote_heads( binding: witness_binding(remote_url: concat(bare, "-missing"), worktree: work), ) let cleaned = shell.Remove.RecursiveForce(path: root.path) - match materialized { + let argv_ok = match materialized { ArgvMaterializationRefused { at: _, cause: _ } => false ArgvMaterialized { argv } => count(argv) >= 3 && argv.first() == "git" } - && setup - && match observed { - RepositoryRemoteHeadsRead { heads } => count(heads) == 1 - RepositoryRemoteHeadProcessRefused { observation: _ } => false - RepositoryRemoteHeadDecodeRefused { cause: _ } => false - } - && match moved { - RepositoryRemoteHeadsRead { heads } => count(heads) == 1 - RepositoryRemoteHeadProcessRefused { observation: _ } => false - RepositoryRemoteHeadDecodeRefused { cause: _ } => false - } - && match unavailable { - RepositoryRemoteHeadProcessRefused { observation: _ } => true - RepositoryRemoteHeadsRead { heads: _ } => false - RepositoryRemoteHeadDecodeRefused { cause: _ } => false - } - && cleaned.success + let observed_ok = match observed { + RepositoryRemoteHeadsRead { heads } => count(heads) == 1 + RepositoryRemoteHeadProcessRefused { observation: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + let moved_ok = match moved { + RepositoryRemoteHeadsRead { heads } => count(heads) == 1 + RepositoryRemoteHeadProcessRefused { observation: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + let unavailable_ok = match unavailable { + RepositoryRemoteHeadProcessRefused { observation: _ } => true + RepositoryRemoteHeadsRead { heads: _ } => false + RepositoryRemoteHeadDecodeRefused { cause: _ } => false + } + setup && pushed && argv_ok && observed_ok && moved_ok && unavailable_ok && cleaned.success } test fn repository_remote_head_keystone_holds_by_real_execution() -> Bool { From 282ac294952821ecc1ebe2b8ceebfde2243cfaac Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:45:47 +0000 Subject: [PATCH 05/10] Fix wet witness parse error: multiline let assignment. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parser rejects `let setup =` followed by a newline before the RHS — a pattern unique to this file in the corpus. Move the git setup && chain into setup_bare_repo(). Co-authored-by: Cursor --- ...ry_remote_head_real_execution_witness_test.dag | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dag/test/claim/repository_remote_head_real_execution_witness_test.dag b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag index 523a0e67515..f72481c108a 100644 --- a/dag/test/claim/repository_remote_head_real_execution_witness_test.dag +++ b/dag/test/claim/repository_remote_head_real_execution_witness_test.dag @@ -37,16 +37,19 @@ fn run(script: String) -> Bool { shell.Exec.Run(script: script as extdeps.shell.exec.TransportScript).success } +fn setup_bare_repo(bare: String, work: String) -> Bool { + run(script: concat("git init --bare ", bare)) + && run(script: concat("git init ", work)) + && run(script: concat("cd ", work, " && git config user.email witness@test && git config user.name witness")) + && run(script: concat("cd ", work, " && git remote add origin ", bare)) + && run(script: concat("cd ", work, " && echo file > f && git add f && git commit -m init && git push -u origin main")) +} + test fn repository_remote_head_bare_repo_by_real_execution() -> Bool { let root = shell.Mktemp.Dir() let bare = concat(root.path as String, "/remote.git") let work = concat(root.path as String, "/work") - let setup = - run(script: concat("git init --bare ", bare)) - && run(script: concat("git init ", work)) - && run(script: concat("cd ", work, " && git config user.email witness@test && git config user.name witness")) - && run(script: concat("cd ", work, " && git remote add origin ", bare)) - && run(script: concat("cd ", work, " && echo file > f && git add f && git commit -m init && git push -u origin main")) + let setup = setup_bare_repo(bare: bare, work: work) let binding = witness_binding(remote_url: bare, worktree: work) let inv = repository_remote_head_invocation(binding: binding) let materialized = shell_materialize_operation_argv( From 91bbe8580fd1af9bb5dd9ff7c791c1e0c0807b4d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 02:08:55 +0000 Subject: [PATCH 06/10] WIP: Direct-process observation substrate --- dag/extdeps/git/ls_remote_advertisement.dag | 37 ++++++------------- dag/gunbc/repository_remote_head.dag | 2 +- ...t_ls_remote_advertisement_witness_test.dag | 2 +- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/dag/extdeps/git/ls_remote_advertisement.dag b/dag/extdeps/git/ls_remote_advertisement.dag index 6481648dc0f..a813a5c66d3 100644 --- a/dag/extdeps/git/ls_remote_advertisement.dag +++ b/dag/extdeps/git/ls_remote_advertisement.dag @@ -1,6 +1,7 @@ module extdeps.git.ls_remote_advertisement import std.types { String, Int, List, NonEmptyStr, Bytes } +import extdeps.git { advertised_ref_branch_name } import extdeps.git.object_store { GitObjectFormat, GitObjectFormatSha1, @@ -11,9 +12,7 @@ import extdeps.git.object_store { git_decode_object_id_text, } -data ls_remote_advertisement_authority_note: String = "Typed decoder for git ls-remote ref-advertisement output. Strict line framing, validated object ids through git_decode_object_id_text, full ref identity, located malformed-line refusal, duplicate-ref refusal. No cast from arbitrary text to CommitSha or NonEmptyStr — those brands are not construction-enforced in this corpus." - -data advertised_ref_heads_prefix: String = "refs/heads/" +data ls_remote_advertisement_authority_note: String = "Typed decoder for git ls-remote ref-advertisement output. Strict line framing, validated object ids through git_decode_object_id_text, full ref identity, located malformed-line refusal, duplicate-ref refusal. Branch projection reuses extdeps.git.advertised_ref_branch_name. No cast from arbitrary text to CommitSha or NonEmptyStr — those brands are not construction-enforced in this corpus." type GitAdvertisedRef { object_id: GitObjectId @@ -51,18 +50,6 @@ type GitAdvertisedRefFold { outcome: GitAdvertisedRefDecodeOutcome } -fn advertised_ref_branch_name(ref_name: String) -> String { - if starts_with(s: ref_name, prefix: advertised_ref_heads_prefix) { - substring( - s: ref_name, - start: advertised_ref_heads_prefix.length(), - end: ref_name.length(), - ) - } else { - ref_name - } -} - type GitAdvertisedRefLineFields { object_id_text: String ref_name: String @@ -93,13 +80,13 @@ fn advertised_ref_line_field_step(acc: GitAdvertisedRefLineFields, part: String) type GitAdvertisedRefLineDecodeOutcome = GitAdvertisedRefLineDecoded { row: GitAdvertisedRef } - | GitAdvertisedRefLineMalformed { line_index: Int, line: String } - | GitAdvertisedRefObjectIdRefused { + | GitAdvertisedRefLineAtMalformed { line_index: Int, line: String } + | GitAdvertisedRefLineAtObjectIdRefused { line_index: Int, line: String, cause: GitObjectIdTextRefusal, } - | GitAdvertisedRefEmptyBranchProjection { + | GitAdvertisedRefLineAtEmptyBranchProjection { line_index: Int, ref_name: String, } @@ -120,18 +107,18 @@ fn git_decode_advertised_ref_line( f: (acc, part) => advertised_ref_line_field_step(acc: acc, part: part), ) if fields.field_count != 2 { - GitAdvertisedRefLineMalformed { line_index: line_index, line: line } + GitAdvertisedRefLineAtMalformed { line_index: line_index, line: line } } else if fields.object_id_text == "" || fields.ref_name == "" { - GitAdvertisedRefLineMalformed { line_index: line_index, line: line } + GitAdvertisedRefLineAtMalformed { line_index: line_index, line: line } } else if advertised_ref_branch_name(ref_name: fields.ref_name) == "" { - GitAdvertisedRefEmptyBranchProjection { + GitAdvertisedRefLineAtEmptyBranchProjection { line_index: line_index, ref_name: fields.ref_name, } } else { match git_decode_object_id_text(format: format, text: fields.object_id_text) { GitObjectIdTextRefused { cause } => - GitAdvertisedRefObjectIdRefused { + GitAdvertisedRefLineAtObjectIdRefused { line_index: line_index, line: line, cause: cause, @@ -163,7 +150,7 @@ fn git_advertised_ref_step( acc } else { match git_decode_advertised_ref_line(line: line, line_index: acc.index, format: format) { - GitAdvertisedRefLineMalformed { line_index, line: bad } => + GitAdvertisedRefLineAtMalformed { line_index, line: bad } => GitAdvertisedRefFold { refs: acc.refs, index: acc.index + 1, @@ -171,7 +158,7 @@ fn git_advertised_ref_step( failed: true, outcome: GitAdvertisedRefLineMalformed { line_index: line_index, line: bad }, } - GitAdvertisedRefObjectIdRefused { line_index, line: bad, cause } => + GitAdvertisedRefLineAtObjectIdRefused { line_index, line: bad, cause } => GitAdvertisedRefFold { refs: acc.refs, index: acc.index + 1, @@ -183,7 +170,7 @@ fn git_advertised_ref_step( cause: cause, }, } - GitAdvertisedRefEmptyBranchProjection { line_index, ref_name } => + GitAdvertisedRefLineAtEmptyBranchProjection { line_index, ref_name } => GitAdvertisedRefFold { refs: acc.refs, index: acc.index + 1, diff --git a/dag/gunbc/repository_remote_head.dag b/dag/gunbc/repository_remote_head.dag index ccfbd48bf8b..3ec2ebe6910 100644 --- a/dag/gunbc/repository_remote_head.dag +++ b/dag/gunbc/repository_remote_head.dag @@ -26,8 +26,8 @@ import extdeps.git.ls_remote_advertisement { GitAdvertisedRefDecodeOutcome, GitAdvertisedRefsDecoded, git_decode_ls_remote_stdout_bytes, - advertised_ref_branch_name, } +import extdeps.git { advertised_ref_branch_name } import extdeps.git.object_store { GitObjectFormatSha1, git_object_id_content_hash } data repository_remote_head_note: String = "Repository-bound remote-head observation. Consumes an admitted GitRepositoryBinding (worktree, remote URL, forge identity, credential binding) — not a free origin string — and routes through v2.std.operation_argv BoundOperationInvocation. Process observation and advertisement decoding are separate layers: gunbc.direct_process_observation classifies the process result; extdeps.git.ls_remote_advertisement decodes stdout bytes." diff --git a/dag/test/claim/git_ls_remote_advertisement_witness_test.dag b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag index 08501f75676..c5b738b01b9 100644 --- a/dag/test/claim/git_ls_remote_advertisement_witness_test.dag +++ b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag @@ -2,6 +2,7 @@ module test.claim.git_ls_remote_advertisement_witness_test import std.types { Bool, String, Int, List, NonEmptyStr, Bytes } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.git { advertised_ref_branch_name } import extdeps.git.ls_remote_advertisement { GitAdvertisedRefDecodeOutcome, GitAdvertisedRefsDecoded, @@ -11,7 +12,6 @@ import extdeps.git.ls_remote_advertisement { GitAdvertisedRefEmptyBranchProjection, git_decode_ls_remote_stdout, git_decode_advertised_ref_line, - advertised_ref_branch_name, } import extdeps.git.object_store { GitObjectFormatSha1, From c0abea8e0bdfe0a709f195efd0d783014826c908 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 02:33:54 +0000 Subject: [PATCH 07/10] Fix signaled-process fail-open and drop plan file from PR. Refuse declared shell exit_code when the process has no exit code (signaled termination) instead of fabricating 0, which made exit_code != 0 guards treat SIGKILL as success. Align exit_success/success with !signaled && exit_code == Some(0). Drop transport_argv plan edits (landed in PR #7527). Fix ls-remote advertisement witness to use a 40-char non-hex object id. Co-authored-by: Cursor --- .../transport_argv_anemia_dissolution.dag | 9 +----- ...irect_process_observation_witness_test.dag | 27 ++++++++++++++++ ...t_ls_remote_advertisement_witness_test.dag | 14 ++++++--- src/v1/stage0/src/v1_interpreter.rs | 31 +++++++++++++++---- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/dag/gunbc/plans/transport_argv_anemia_dissolution.dag b/dag/gunbc/plans/transport_argv_anemia_dissolution.dag index 8cce2356714..dfc92f598af 100644 --- a/dag/gunbc/plans/transport_argv_anemia_dissolution.dag +++ b/dag/gunbc/plans/transport_argv_anemia_dissolution.dag @@ -43,7 +43,7 @@ fn taad_target() -> List { fn taad_lanes() -> List { [ - h2(text: "Three lanes"), + h2(text: "Two lanes"), h3(text: "Lane A — HTTP transport dissolution (curl argv -> transport rest)"), ol(items: [ li(text: "Extend the rest model with the 4 items above (3 axes + modeled success)."), @@ -57,13 +57,6 @@ fn taad_lanes() -> List { li(text: "Each shell tool becomes a modeled extdeps/tools/* service with typed operations — precedent already exists (sha256sum, jq, xorriso, sed, grep are modeled). The build sequence becomes typed tool ops, not a bare argv list."), li(text: "This is the broader §2 argv anemia and rides the shell -> intent lane (the intent IS the .dag graph; emit vs realization). Model just-in-time per real consumer, never a big-bang sweep."), ]), - h3(text: "Lane C — direct-process observation substrate (E1–E3)"), - ol(items: [ - li(text: "E1 — minimum process observation shape in gunbc.direct_process_observation: completed vs spawn-refused vs wait-refused; exited vs signaled vs timed-out vs killed; no success Bool; no -1 exit sentinel; faithful byte streams at the boundary."), - li(text: "E2 — repository-bound remote-head operation in gunbc.repository_remote_head on v2.std.operation_argv BoundOperationInvocation (worktree + remote URL + forge identity + credential binding)."), - li(text: "E3 — typed ls-remote advertisement decoder in extdeps.git.ls_remote_advertisement with validated object ids, duplicate-ref refusal, and located malformed-line refusal."), - li(text: "Land consumed: E2/E3 are the first consumers of E1 in the same stack; witness by execution against a temporary local bare repository."), - ]), ] } diff --git a/dag/test/claim/direct_process_observation_witness_test.dag b/dag/test/claim/direct_process_observation_witness_test.dag index 5637221e4eb..51b1e50eea0 100644 --- a/dag/test/claim/direct_process_observation_witness_test.dag +++ b/dag/test/claim/direct_process_observation_witness_test.dag @@ -109,9 +109,36 @@ fn absent_exit_code_becomes_killed_not_minus_one() -> Bool { } } +fn signaled_termination_is_not_exited_zero() -> Bool { + match direct_process_observation_from_shell( + spawn_ok: true, + wait_ok: true, + timed_out: false, + exit_code: 0, + signaled: true, + signal: 9, + exit_code_present: false, + stdout: utf8_encode_bytes(s: ""), + stderr: utf8_encode_bytes(s: ""), + spawn_detail: "", + wait_detail: "", + ) { + ProcessCompleted { termination, streams: _ } => + match termination { + ProcessSignaled { signal } => signal == 9 + ProcessExited { code: _ } => false + ProcessTimedOut => false + ProcessKilled => false + } + ProcessSpawnRefused { detail: _ } => false + ProcessWaitRefused { detail: _ } => false + } +} + test fn direct_process_observation_keystone_holds() -> Bool { spawn_refusal_is_typed() && wait_refusal_is_typed() && exited_termination_carries_code_without_sentinel() && absent_exit_code_becomes_killed_not_minus_one() + && signaled_termination_is_not_exited_zero() } diff --git a/dag/test/claim/git_ls_remote_advertisement_witness_test.dag b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag index c5b738b01b9..08c18b42638 100644 --- a/dag/test/claim/git_ls_remote_advertisement_witness_test.dag +++ b/dag/test/claim/git_ls_remote_advertisement_witness_test.dag @@ -10,6 +10,10 @@ import extdeps.git.ls_remote_advertisement { GitAdvertisedRefObjectIdRefused, GitAdvertisedRefDuplicate, GitAdvertisedRefEmptyBranchProjection, + GitAdvertisedRefLineAtObjectIdRefused, + GitAdvertisedRefLineAtMalformed, + GitAdvertisedRefLineAtEmptyBranchProjection, + GitAdvertisedRefLineDecoded, git_decode_ls_remote_stdout, git_decode_advertised_ref_line, } @@ -52,20 +56,22 @@ fn malformed_line_refuses_at_index() -> Bool { } } +data fixture_sha_invalid: String = "gggggggggggggggggggggggggggggggggggggggg" + fn invalid_object_id_refuses_not_brands() -> Bool { match git_decode_advertised_ref_line( - line: line_of(sha: "not-hex", ref_name: "refs/heads/main"), + line: line_of(sha: fixture_sha_invalid, ref_name: "refs/heads/main"), line_index: 0, format: GitObjectFormatSha1, ) { - GitAdvertisedRefObjectIdRefused { cause, line_index: _, line: _ } => + GitAdvertisedRefLineAtObjectIdRefused { cause, line_index: _, line: _ } => match cause { GitObjectIdTextNonHexadecimal => true _ => false } GitAdvertisedRefLineDecoded { row: _ } => false - GitAdvertisedRefLineMalformed { line_index: _, line: _ } => false - GitAdvertisedRefEmptyBranchProjection { line_index: _, ref_name: _ } => false + GitAdvertisedRefLineAtMalformed { line_index: _, line: _ } => false + GitAdvertisedRefLineAtEmptyBranchProjection { line_index: _, ref_name: _ } => false } } diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index f94be496f36..b725219578e 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5731,6 +5731,26 @@ fn shell_stdout_string_lossy(bytes: &[u8]) -> String { String::from_utf8_lossy(bytes).into_owned() } +fn shell_exit_succeeded(result: &ShellResult) -> bool { + !result.signaled && result.exit_code == Some(0) +} + +fn shell_declared_exit_code(result: &ShellResult) -> InterpResult { + match result.exit_code { + Some(code) => Ok(i64::from(code)), + None => Err(InterpError::TypeError { + msg: if result.signaled { + format!( + "declared shell exit_code is absent: process terminated by signal {}", + result.signal + ) + } else { + "declared shell exit_code is absent".to_string() + }, + }), + } +} + fn push_shell_argv_tokens(argv: &mut Vec, val: Value) -> InterpResult<()> { match &val { Value::Str(s) => { @@ -7014,7 +7034,6 @@ fn map_shell_outputs( let field_name = authored_name_at(ctx.si(), child.clone()); let from_key = extract_from_key(child, ctx); let is_optional_field = child.return_cardinality == Cardinality::CardOptional; - let exit_code_i64 = result.exit_code.map(i64::from).unwrap_or(0); let process_failed = result.signaled || result.exit_code != Some(0); let stdout_str = shell_stdout_string_lossy(&result.stdout_raw); let stderr_str = shell_stdout_string_lossy(&result.stderr_raw); @@ -7025,8 +7044,8 @@ fn map_shell_outputs( Some("stderr") => Value::Str(stderr_str.clone()), Some("stdout_bytes") => bytes_to_value(&result.stdout_raw), Some("stderr_bytes") => bytes_to_value(&result.stderr_raw), - Some("exit_success") => Value::Bool(result.exit_code == Some(0)), - Some("exit_code") => Value::Int(exit_code_i64), + Some("exit_success") => Value::Bool(shell_exit_succeeded(result)), + Some("exit_code") => Value::Int(shell_declared_exit_code(result)?), Some("exit_code_present") => Value::Bool(result.exit_code.is_some()), Some("signaled") => Value::Bool(result.signaled), Some("signal") => Value::Int(result.signal as i64), @@ -7038,8 +7057,8 @@ fn map_shell_outputs( list_value((lines)) } _ => match field_name.as_str() { - "success" => Value::Bool(result.exit_code == Some(0)), - "exit_code" => Value::Int(exit_code_i64), + "success" => Value::Bool(shell_exit_succeeded(result)), + "exit_code" => Value::Int(shell_declared_exit_code(result)?), "exit_code_present" => Value::Bool(result.exit_code.is_some()), "signaled" => Value::Bool(result.signaled), "signal" => Value::Int(result.signal as i64), @@ -7047,7 +7066,7 @@ fn map_shell_outputs( "stderr" => Value::Str(stderr_str.clone()), "stdout_bytes" => bytes_to_value(&result.stdout_raw), "stderr_bytes" => bytes_to_value(&result.stderr_raw), - "exists" => Value::Bool(result.exit_code == Some(0)), + "exists" => Value::Bool(shell_exit_succeeded(result)), _ => Value::Null, }, }; From bd06993103427824257a5a7f562dbe50fcd0d8ef Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 02:44:04 +0000 Subject: [PATCH 08/10] Fix regen workdir newline: trim string stdout projection only. git.Core.Toplevel feeds cargo.Build.BuildInheritEnv workdir from the lossy stdout string. Removing trim_end at the shell boundary let rev-parse's trailing newline into the path (env -C .../gunbc\n). stdout_bytes/stderr_bytes stay faithful; only the String projection trims trailing whitespace like main. Co-authored-by: Cursor --- src/v1/stage0/src/v1_interpreter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index b725219578e..05c864cb787 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5728,7 +5728,8 @@ fn bytes_to_value(bytes: &[u8]) -> Value { } fn shell_stdout_string_lossy(bytes: &[u8]) -> String { - String::from_utf8_lossy(bytes).into_owned() + // String stdout is a lossy CLI projection; stdout_bytes retains faithful bytes. + String::from_utf8_lossy(bytes).trim_end().to_string() } fn shell_exit_succeeded(result: &ShellResult) -> bool { From 59d333c9bb0077b7280ca0c9bb4e1c46bf096266 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 03:15:18 +0000 Subject: [PATCH 09/10] WIP: Direct-process observation substrate --- dag/extdeps/git/ls_remote_advertisement.dag | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dag/extdeps/git/ls_remote_advertisement.dag b/dag/extdeps/git/ls_remote_advertisement.dag index a813a5c66d3..5618dcef3ac 100644 --- a/dag/extdeps/git/ls_remote_advertisement.dag +++ b/dag/extdeps/git/ls_remote_advertisement.dag @@ -1,5 +1,12 @@ module extdeps.git.ls_remote_advertisement +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "git-scm.com/docs/git-ls-remote" + } +} + import std.types { String, Int, List, NonEmptyStr, Bytes } import extdeps.git { advertised_ref_branch_name } import extdeps.git.object_store { From f947013fe03b0a14f08207cb4f9cb6277d3a7707 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 03:21:40 +0000 Subject: [PATCH 10/10] Add extdeps authority anchor to ls_remote_advertisement. The mandatory-tag corpus wall reds extdeps modules without extdeps_external_authority_anchor; this blocked cheap_claim_pool_gate. Co-authored-by: Cursor --- dag/extdeps/git/ls_remote_advertisement.dag | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dag/extdeps/git/ls_remote_advertisement.dag b/dag/extdeps/git/ls_remote_advertisement.dag index 5618dcef3ac..0b0a6b72822 100644 --- a/dag/extdeps/git/ls_remote_advertisement.dag +++ b/dag/extdeps/git/ls_remote_advertisement.dag @@ -1,12 +1,5 @@ module extdeps.git.ls_remote_advertisement -data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { - uri: Uri { - scheme: Https - locator: "git-scm.com/docs/git-ls-remote" - } -} - import std.types { String, Int, List, NonEmptyStr, Bytes } import extdeps.git { advertised_ref_branch_name } import extdeps.git.object_store { @@ -19,6 +12,13 @@ import extdeps.git.object_store { git_decode_object_id_text, } +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "git-scm.com/docs/git-ls-remote" + } +} + data ls_remote_advertisement_authority_note: String = "Typed decoder for git ls-remote ref-advertisement output. Strict line framing, validated object ids through git_decode_object_id_text, full ref identity, located malformed-line refusal, duplicate-ref refusal. Branch projection reuses extdeps.git.advertised_ref_branch_name. No cast from arbitrary text to CommitSha or NonEmptyStr — those brands are not construction-enforced in this corpus." type GitAdvertisedRef {