@team-agent/installer 0.2.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -457
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -86
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1239
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -143
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -602
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
//! codex startup-prompt recognizer — workspace-trust + update-skip screen detection.
|
|
2
|
+
//!
|
|
3
|
+
//! Golden (READ-ONLY truth `team-agent-public` v0.2.11): `provider_cli/codex.py`
|
|
4
|
+
//! - `CodexAdapter.handle_startup_prompts` (:142-182)
|
|
5
|
+
//! - `maybe_skip_update_prompt` (:262-268)
|
|
6
|
+
//!
|
|
7
|
+
//! recognizer-class (Gap 29 — burned 4 Mac minis): a NAIVE substring port gets the RECENCY命门 wrong.
|
|
8
|
+
//! A prompt is acted on ONLY when its `rfind` position is GREATER than the ready marker's `rfind`
|
|
9
|
+
//! position (i.e. it appears LATER / more recently in the captured scrollback). A stale prompt ABOVE an
|
|
10
|
+
//! already-ready marker is left alone — ready wins and polling stops. RED-first skeleton; porter-d
|
|
11
|
+
//! implements GREEN black-box against golden codex.py.
|
|
12
|
+
|
|
13
|
+
use std::time::Duration;
|
|
14
|
+
|
|
15
|
+
use crate::transport::{CaptureRange, Key, Target, Transport};
|
|
16
|
+
|
|
17
|
+
const TRUST_MARKERS: &[&str] = &[
|
|
18
|
+
"Do you trust the contents of this directory?",
|
|
19
|
+
"Do you trust the files in this folder?",
|
|
20
|
+
"Do you trust this folder?",
|
|
21
|
+
];
|
|
22
|
+
const UPDATE_MARKERS: &[&str] = &["Update available!", "Update now"];
|
|
23
|
+
/// Plain ready markers (not the bare `›` glyph — that glyph also indicates a
|
|
24
|
+
/// numbered-menu selector and is handled by [`rightmost_input_prompt_glyph`] with
|
|
25
|
+
/// shape gating per N15 / CR-063: detect by SHAPE, not a single Unicode codepoint).
|
|
26
|
+
const READY_MARKERS: &[&str] = &["OpenAI Codex", "codex>"];
|
|
27
|
+
|
|
28
|
+
/// Per-poll decision for the codex startup screen. Golden order each iteration (codex.py:160-181):
|
|
29
|
+
/// update-skip is checked FIRST, then workspace-trust, then ready (stop), else keep polling.
|
|
30
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
31
|
+
pub enum StartupScreenDecision {
|
|
32
|
+
/// `maybe_skip_update_prompt` matched: update_pos >= 0 && update_pos > ready_pos (codex.py:262-267).
|
|
33
|
+
SkipUpdatePrompt,
|
|
34
|
+
/// workspace-trust: trust_pos >= 0 && trust_pos > ready_pos (codex.py:166-174).
|
|
35
|
+
AnswerWorkspaceTrust,
|
|
36
|
+
/// ready_pos >= 0 with no actionable prompt above it (codex.py:178) -> stop polling.
|
|
37
|
+
Ready,
|
|
38
|
+
/// none of the above (codex.py:180) -> sleep + keep polling.
|
|
39
|
+
KeepPolling,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// One handled startup prompt — an entry of golden's `handled` list.
|
|
43
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
44
|
+
pub struct HandledPrompt {
|
|
45
|
+
pub prompt: String,
|
|
46
|
+
pub action: String,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// PURE recognizer (codex.py:160-181 + maybe_skip_update_prompt :262-268): captured scrollback ->
|
|
50
|
+
/// decision. NO IO. The RECENCY命门: a prompt is acted on ONLY when its `rfind` position is strictly
|
|
51
|
+
/// GREATER than the ready marker's `rfind` position. update is evaluated before trust.
|
|
52
|
+
/// trust strings (rfind max of): "Do you trust the contents of this directory?" /
|
|
53
|
+
/// "Do you trust the files in this folder?" / "Do you trust this folder?"
|
|
54
|
+
/// update strings (rfind max of): "Update available!" / "Update now"
|
|
55
|
+
/// ready markers (rfind max of): "OpenAI Codex" / "›" / "codex>"
|
|
56
|
+
pub fn classify_codex_startup_screen(output: &str) -> StartupScreenDecision {
|
|
57
|
+
// CR-063 / subroot real-machine residual: actionable-shape override BEFORE recency.
|
|
58
|
+
// The recency model ("prompt above ready = stale-scrolled, ignore") assumes the
|
|
59
|
+
// active state is the LATEST byte on screen. Real Codex breaks that assumption:
|
|
60
|
+
// while a trust modal is still active, Codex pre-renders the Update box, the
|
|
61
|
+
// OpenAI Codex banner, AND a bottom `› Find and fix a bug…` input-prompt indicator
|
|
62
|
+
// BELOW the trust menu — so recency would mark the screen Ready and the trust
|
|
63
|
+
// menu would never be answered. When the captured text has the actionable trust
|
|
64
|
+
// shape (`Do you trust …` phrase + a `› <digit>. ` numbered-menu line, N15),
|
|
65
|
+
// the modal IS the live state regardless of what comes after it. Return early.
|
|
66
|
+
if has_actionable_trust_shape(output) {
|
|
67
|
+
return StartupScreenDecision::AnswerWorkspaceTrust;
|
|
68
|
+
}
|
|
69
|
+
// N15/CR-063 root-cause (recency lane): the bare `›` glyph is BOTH the Codex
|
|
70
|
+
// input-prompt indicator AND the numbered-menu selector on a real trust pane
|
|
71
|
+
// (`› 1. Yes, continue`). Detect by SHAPE: `›` is a ready marker only when its
|
|
72
|
+
// tail is NOT a `<digit>. ` menu item.
|
|
73
|
+
let ready_pos = max_two(
|
|
74
|
+
max_rfind(output, READY_MARKERS),
|
|
75
|
+
rightmost_input_prompt_glyph(output),
|
|
76
|
+
);
|
|
77
|
+
if is_more_recent(max_rfind(output, UPDATE_MARKERS), ready_pos) {
|
|
78
|
+
return StartupScreenDecision::SkipUpdatePrompt;
|
|
79
|
+
}
|
|
80
|
+
if is_more_recent(max_rfind(output, TRUST_MARKERS), ready_pos) {
|
|
81
|
+
return StartupScreenDecision::AnswerWorkspaceTrust;
|
|
82
|
+
}
|
|
83
|
+
if ready_pos.is_some() {
|
|
84
|
+
StartupScreenDecision::Ready
|
|
85
|
+
} else {
|
|
86
|
+
StartupScreenDecision::KeepPolling
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Actionable trust shape (N15): the captured text contains a trust phrase AND a
|
|
91
|
+
/// numbered-menu selector line `› <digit>. `. This is the modal-still-active signal
|
|
92
|
+
/// that survives Codex's pre-rendering of the banner/input prompt below the menu.
|
|
93
|
+
/// Does NOT match a single-screen "trust phrase + bare `›`" (e.g. plain Ready
|
|
94
|
+
/// follow-up text), so historical "trust ABOVE ready" recency tests keep passing
|
|
95
|
+
/// — those fixtures do not include a `› <digit>. ` menu line.
|
|
96
|
+
fn has_actionable_trust_shape(output: &str) -> bool {
|
|
97
|
+
if !TRUST_MARKERS.iter().any(|marker| output.contains(marker)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
contains_numbered_menu_glyph(output)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// `true` iff any `›` in the output is followed by a numbered-menu selector
|
|
104
|
+
/// (` <digit>. `). The shape pairs the glyph with a digit-dot line item — the
|
|
105
|
+
/// Codex trust/update menu printing convention.
|
|
106
|
+
fn contains_numbered_menu_glyph(output: &str) -> bool {
|
|
107
|
+
let glyph = '›';
|
|
108
|
+
let glyph_len = glyph.len_utf8();
|
|
109
|
+
let mut start = 0;
|
|
110
|
+
while let Some(rel) = output[start..].find(glyph) {
|
|
111
|
+
let abs = start + rel;
|
|
112
|
+
let tail_start = abs + glyph_len;
|
|
113
|
+
if tail_start > output.len() {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if is_numbered_menu_tail(&output[tail_start..]) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
start = tail_start;
|
|
120
|
+
}
|
|
121
|
+
false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Rightmost `›` whose tail is NOT a numbered-menu selector (` <digit>. `). A bare
|
|
125
|
+
/// `›` followed by free text or whitespace is the Codex main-input prompt indicator;
|
|
126
|
+
/// a `›` followed by `1. Yes, continue` is part of the trust/update menu and is NOT
|
|
127
|
+
/// a ready signal.
|
|
128
|
+
fn rightmost_input_prompt_glyph(output: &str) -> Option<usize> {
|
|
129
|
+
let glyph = '›';
|
|
130
|
+
let glyph_len = glyph.len_utf8();
|
|
131
|
+
let mut best = None;
|
|
132
|
+
let bytes = output.as_bytes();
|
|
133
|
+
let mut start = 0;
|
|
134
|
+
while let Some(rel) = output[start..].find(glyph) {
|
|
135
|
+
let abs = start + rel;
|
|
136
|
+
let tail_start = abs + glyph_len;
|
|
137
|
+
if tail_start <= bytes.len() && !is_numbered_menu_tail(&output[tail_start..]) {
|
|
138
|
+
best = Some(abs);
|
|
139
|
+
}
|
|
140
|
+
start = tail_start;
|
|
141
|
+
if start > output.len() {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
best
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn is_numbered_menu_tail(tail: &str) -> bool {
|
|
149
|
+
let trimmed = tail.trim_start_matches(' ');
|
|
150
|
+
let mut chars = trimmed.chars();
|
|
151
|
+
matches!(
|
|
152
|
+
(chars.next(), chars.next()),
|
|
153
|
+
(Some(d), Some('.')) if d.is_ascii_digit()
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn max_two(a: Option<usize>, b: Option<usize>) -> Option<usize> {
|
|
158
|
+
match (a, b) {
|
|
159
|
+
(Some(x), Some(y)) => Some(x.max(y)),
|
|
160
|
+
(Some(x), None) | (None, Some(x)) => Some(x),
|
|
161
|
+
(None, None) => None,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Capture-poll loop (codex.py:142-182) over the `transport.capture()` seam (NOT a raw subprocess, so
|
|
166
|
+
/// it stays unit-testable). On `AnswerWorkspaceTrust` -> send `Enter` + push
|
|
167
|
+
/// {prompt:"codex_workspace_trust", action:"sent_enter"}; on `SkipUpdatePrompt` -> send `Down`,`Enter`
|
|
168
|
+
/// + push {prompt:"codex_update_available", action:"sent_skip"}; on `Ready` -> stop. Loops up to
|
|
169
|
+
/// `checks` (golden default 30), `sleep_s` (golden 0.5) between iterations. Returns the ordered
|
|
170
|
+
/// `handled` list. Capture is full scrollback (golden `tmux capture-pane -p -S - -t <target>`).
|
|
171
|
+
pub fn codex_handle_startup_prompts(
|
|
172
|
+
transport: &dyn Transport,
|
|
173
|
+
target: &Target,
|
|
174
|
+
checks: usize,
|
|
175
|
+
sleep_s: f64,
|
|
176
|
+
) -> Vec<HandledPrompt> {
|
|
177
|
+
let mut handled = Vec::new();
|
|
178
|
+
for _ in 0..checks {
|
|
179
|
+
let screen = match transport.capture(target, CaptureRange::Full) {
|
|
180
|
+
Ok(captured) => captured.text,
|
|
181
|
+
Err(_) => String::new(),
|
|
182
|
+
};
|
|
183
|
+
match classify_codex_startup_screen(&screen) {
|
|
184
|
+
StartupScreenDecision::SkipUpdatePrompt => {
|
|
185
|
+
let _ = transport.send_keys(target, &[Key::Down, Key::Enter]);
|
|
186
|
+
handled.push(HandledPrompt {
|
|
187
|
+
prompt: "codex_update_available".to_string(),
|
|
188
|
+
action: "sent_skip".to_string(),
|
|
189
|
+
});
|
|
190
|
+
sleep_between_polls(sleep_s);
|
|
191
|
+
}
|
|
192
|
+
StartupScreenDecision::AnswerWorkspaceTrust => {
|
|
193
|
+
let _ = transport.send_keys(target, &[Key::Enter]);
|
|
194
|
+
handled.push(HandledPrompt {
|
|
195
|
+
prompt: "codex_workspace_trust".to_string(),
|
|
196
|
+
action: "sent_enter".to_string(),
|
|
197
|
+
});
|
|
198
|
+
sleep_between_polls(sleep_s);
|
|
199
|
+
}
|
|
200
|
+
StartupScreenDecision::Ready => break,
|
|
201
|
+
StartupScreenDecision::KeepPolling => sleep_between_polls(sleep_s),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
handled
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn max_rfind(output: &str, needles: &[&str]) -> Option<usize> {
|
|
208
|
+
needles.iter().filter_map(|needle| output.rfind(needle)).max()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn is_more_recent(prompt_pos: Option<usize>, ready_pos: Option<usize>) -> bool {
|
|
212
|
+
match (prompt_pos, ready_pos) {
|
|
213
|
+
(Some(prompt), Some(ready)) => prompt > ready,
|
|
214
|
+
(Some(_), None) => true,
|
|
215
|
+
_ => false,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn sleep_between_polls(sleep_s: f64) {
|
|
220
|
+
let millis = (sleep_s * 1000.0).round();
|
|
221
|
+
if millis.is_finite() && millis > 0.0 && millis <= u64::MAX as f64 {
|
|
222
|
+
std::thread::sleep(Duration::from_millis(millis as u64));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[cfg(test)]
|
|
227
|
+
mod tests {
|
|
228
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
229
|
+
use super::*;
|
|
230
|
+
use crate::model::enums::PaneLiveness;
|
|
231
|
+
use crate::transport::{
|
|
232
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, Key,
|
|
233
|
+
PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome, SpawnResult, Target, TransportError,
|
|
234
|
+
WindowName,
|
|
235
|
+
};
|
|
236
|
+
use std::collections::BTreeMap;
|
|
237
|
+
use std::path::Path;
|
|
238
|
+
use std::sync::Mutex;
|
|
239
|
+
|
|
240
|
+
// ── EXACT golden strings (provider_cli/codex.py). Do not paraphrase — recognizer-class. ──────────
|
|
241
|
+
const TRUST_DIR: &str = "Do you trust the contents of this directory?";
|
|
242
|
+
const TRUST_FILES: &str = "Do you trust the files in this folder?";
|
|
243
|
+
const TRUST_FOLDER: &str = "Do you trust this folder?";
|
|
244
|
+
const UPDATE_AVAIL: &str = "Update available!";
|
|
245
|
+
const UPDATE_NOW: &str = "Update now";
|
|
246
|
+
const READY_BANNER: &str = "OpenAI Codex";
|
|
247
|
+
const READY_PROMPT: &str = "›"; // U+203A
|
|
248
|
+
const READY_BARE: &str = "codex>";
|
|
249
|
+
|
|
250
|
+
// ── ① + ② RED核心 — workspace-trust MORE RECENT than ready -> answer it ──────────────────────────
|
|
251
|
+
#[test]
|
|
252
|
+
fn trust_more_recent_than_ready_answers_workspace_trust() {
|
|
253
|
+
// ready banner appears early; the trust prompt appears LATER; no ready marker after it
|
|
254
|
+
// => trust_pos > ready_pos => answer.
|
|
255
|
+
let screen = format!("{READY_BANNER} v1.2\nwelcome\n\n{TRUST_DIR}\n hit enter ");
|
|
256
|
+
assert_eq!(
|
|
257
|
+
classify_codex_startup_screen(&screen),
|
|
258
|
+
StartupScreenDecision::AnswerWorkspaceTrust
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── ② 命门 CORE — a STALE trust prompt ABOVE the ready marker is NOT answered (ready wins) ────────
|
|
263
|
+
#[test]
|
|
264
|
+
fn stale_trust_above_ready_is_not_answered_ready_wins() {
|
|
265
|
+
// trust prompt FIRST, then a ready marker LATER => trust_pos < ready_pos => do NOT answer.
|
|
266
|
+
// This is the positional-recency命门 a naive substring port gets wrong (would re-send Enter).
|
|
267
|
+
let screen = format!("{TRUST_DIR}\n[trusted earlier]\n{READY_BANNER} ready\n{READY_PROMPT} ");
|
|
268
|
+
assert_eq!(
|
|
269
|
+
classify_codex_startup_screen(&screen),
|
|
270
|
+
StartupScreenDecision::Ready,
|
|
271
|
+
"RECENCY命门: a trust prompt ABOVE the ready marker is stale; ready wins, NO Enter sent"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn each_trust_string_recognized_when_more_recent() {
|
|
277
|
+
for s in [TRUST_DIR, TRUST_FILES, TRUST_FOLDER] {
|
|
278
|
+
let screen = format!("{READY_BANNER}\n...banner...\n{s}\n");
|
|
279
|
+
assert_eq!(
|
|
280
|
+
classify_codex_startup_screen(&screen),
|
|
281
|
+
StartupScreenDecision::AnswerWorkspaceTrust,
|
|
282
|
+
"trust string {s:?} after ready must answer"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── ③ sibling — update-skip recognizer (maybe_skip_update_prompt), same recency命门 ──────────────
|
|
288
|
+
#[test]
|
|
289
|
+
fn update_more_recent_than_ready_skips_update() {
|
|
290
|
+
for s in [UPDATE_AVAIL, UPDATE_NOW] {
|
|
291
|
+
let screen = format!("{READY_BANNER}\nblah\n{s}\n");
|
|
292
|
+
assert_eq!(
|
|
293
|
+
classify_codex_startup_screen(&screen),
|
|
294
|
+
StartupScreenDecision::SkipUpdatePrompt,
|
|
295
|
+
"update string {s:?} after ready must skip"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn stale_update_above_ready_is_not_skipped_ready_wins() {
|
|
302
|
+
let screen = format!("{UPDATE_AVAIL}\n{READY_BANNER}\n{READY_PROMPT} ");
|
|
303
|
+
assert_eq!(classify_codex_startup_screen(&screen), StartupScreenDecision::Ready);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── golden ORDER — update is checked BEFORE trust (both more recent) -> SkipUpdatePrompt wins ─────
|
|
307
|
+
#[test]
|
|
308
|
+
fn update_checked_before_trust() {
|
|
309
|
+
// both update + trust appear after the ready marker; golden runs maybe_skip_update_prompt
|
|
310
|
+
// FIRST each iteration -> the screen resolves to SkipUpdatePrompt, not AnswerWorkspaceTrust.
|
|
311
|
+
let screen = format!("{READY_BANNER}\n{TRUST_DIR}\n{UPDATE_AVAIL}\n");
|
|
312
|
+
assert_eq!(classify_codex_startup_screen(&screen), StartupScreenDecision::SkipUpdatePrompt);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── ready-only / neither ─────────────────────────────────────────────────────────────────────────
|
|
316
|
+
#[test]
|
|
317
|
+
fn each_ready_marker_alone_is_ready() {
|
|
318
|
+
for m in [READY_BANNER, READY_PROMPT, READY_BARE] {
|
|
319
|
+
let screen = format!("booting...\n{m} ");
|
|
320
|
+
assert_eq!(
|
|
321
|
+
classify_codex_startup_screen(&screen),
|
|
322
|
+
StartupScreenDecision::Ready,
|
|
323
|
+
"ready marker {m:?} alone must be Ready"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn no_prompt_no_ready_keeps_polling() {
|
|
330
|
+
assert_eq!(
|
|
331
|
+
classify_codex_startup_screen("loading spinner...\nstill starting\n"),
|
|
332
|
+
StartupScreenDecision::KeepPolling
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── ④ transport.capture() SEAM — the loop answers trust then breaks on ready, via the seam ───────
|
|
337
|
+
/// Scripted transport: `capture` pops the next canned screen; `send_keys` records the keys. All
|
|
338
|
+
/// other methods are unreachable by the startup-prompt loop.
|
|
339
|
+
struct ScriptedTransport {
|
|
340
|
+
screens: Mutex<Vec<String>>,
|
|
341
|
+
sent: Mutex<Vec<Vec<Key>>>,
|
|
342
|
+
}
|
|
343
|
+
impl Transport for ScriptedTransport {
|
|
344
|
+
fn kind(&self) -> BackendKind {
|
|
345
|
+
BackendKind::Tmux
|
|
346
|
+
}
|
|
347
|
+
fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
348
|
+
unimplemented!("not reached by startup-prompt loop")
|
|
349
|
+
}
|
|
350
|
+
fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
|
|
351
|
+
unimplemented!("not reached by startup-prompt loop")
|
|
352
|
+
}
|
|
353
|
+
fn inject(&self, _t: &Target, _p: &InjectPayload, _submit: Key, _b: bool) -> Result<InjectReport, TransportError> {
|
|
354
|
+
unimplemented!("not reached")
|
|
355
|
+
}
|
|
356
|
+
fn send_keys(&self, _t: &Target, keys: &[Key]) -> Result<(), TransportError> {
|
|
357
|
+
self.sent.lock().unwrap().push(keys.to_vec());
|
|
358
|
+
Ok(())
|
|
359
|
+
}
|
|
360
|
+
fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
|
|
361
|
+
let mut q = self.screens.lock().unwrap();
|
|
362
|
+
let text = if q.is_empty() { String::new() } else { q.remove(0) };
|
|
363
|
+
Ok(CapturedText { text, range })
|
|
364
|
+
}
|
|
365
|
+
fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
|
|
366
|
+
Ok(None)
|
|
367
|
+
}
|
|
368
|
+
fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
369
|
+
unimplemented!("not reached")
|
|
370
|
+
}
|
|
371
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
372
|
+
unimplemented!("not reached")
|
|
373
|
+
}
|
|
374
|
+
fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
|
|
375
|
+
Ok(true)
|
|
376
|
+
}
|
|
377
|
+
fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
378
|
+
unimplemented!("not reached")
|
|
379
|
+
}
|
|
380
|
+
fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
|
|
381
|
+
unimplemented!("not reached")
|
|
382
|
+
}
|
|
383
|
+
fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
|
|
384
|
+
unimplemented!("not reached")
|
|
385
|
+
}
|
|
386
|
+
fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
|
|
387
|
+
unimplemented!("not reached")
|
|
388
|
+
}
|
|
389
|
+
fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
390
|
+
unimplemented!("not reached")
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn loop_answers_trust_then_breaks_on_ready_via_capture_seam() {
|
|
396
|
+
let t = ScriptedTransport {
|
|
397
|
+
screens: Mutex::new(vec![
|
|
398
|
+
// iter 1: trust prompt more recent than ready -> answer (send Enter) + continue.
|
|
399
|
+
format!("{READY_BANNER}\n{TRUST_DIR}\n"),
|
|
400
|
+
// iter 2: ready marker, no actionable prompt above it -> break.
|
|
401
|
+
format!("{READY_BANNER} ready\n{READY_PROMPT} "),
|
|
402
|
+
]),
|
|
403
|
+
sent: Mutex::new(Vec::new()),
|
|
404
|
+
};
|
|
405
|
+
let target = Target::Pane(PaneId::new("%1"));
|
|
406
|
+
|
|
407
|
+
let handled = codex_handle_startup_prompts(&t, &target, 5, 0.0);
|
|
408
|
+
|
|
409
|
+
assert_eq!(
|
|
410
|
+
handled,
|
|
411
|
+
vec![HandledPrompt {
|
|
412
|
+
prompt: "codex_workspace_trust".to_string(),
|
|
413
|
+
action: "sent_enter".to_string(),
|
|
414
|
+
}],
|
|
415
|
+
"the loop must answer the workspace-trust prompt exactly once, then break on ready"
|
|
416
|
+
);
|
|
417
|
+
let sent = t.sent.lock().unwrap();
|
|
418
|
+
assert!(
|
|
419
|
+
sent.iter().any(|keys| keys.as_slice() == [Key::Enter]),
|
|
420
|
+
"on workspace-trust the loop must send Enter via the transport.capture() seam; got {sent:?}"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn abnormal_dedup_key_uses_signature_and_optional_turn_id() {
|
|
3
|
+
// probe(/tmp/probe_idle.py read_fault_facts): the C8 dedup key is
|
|
4
|
+
// (Signature, Option<TurnId>). Golden facts below are the exact extraction.
|
|
5
|
+
|
|
6
|
+
// claude api_error → signature=api_error, turn_id=sess-9 (sessionId), kind=error.
|
|
7
|
+
let api_err = [serde_json::json!(
|
|
8
|
+
{"type":"system","subtype":"api_error","level":"error","sessionId":"sess-9"}
|
|
9
|
+
)];
|
|
10
|
+
let f = read_fault_facts(&api_err, Provider::Claude);
|
|
11
|
+
assert_eq!(f.len(), 1);
|
|
12
|
+
assert_eq!(f[0].signature, Signature::new("api_error"));
|
|
13
|
+
assert_eq!(f[0].turn_id, Some(TurnId::new("sess-9")));
|
|
14
|
+
assert_eq!(f[0].kind, FactKind::Error);
|
|
15
|
+
|
|
16
|
+
// claude tool_result is_error → signature=tool_result_is_error,
|
|
17
|
+
// turn_id=parentUuid ("p-1"), kind=error.
|
|
18
|
+
let tool_err = [serde_json::json!(
|
|
19
|
+
{"type":"user","uuid":"u","parentUuid":"p-1",
|
|
20
|
+
"message":{"content":[{"type":"tool_result","is_error":true}]}}
|
|
21
|
+
)];
|
|
22
|
+
let f = read_fault_facts(&tool_err, Provider::Claude);
|
|
23
|
+
assert_eq!(f.len(), 1);
|
|
24
|
+
assert_eq!(f[0].signature, Signature::new("tool_result_is_error"));
|
|
25
|
+
assert_eq!(f[0].turn_id, Some(TurnId::new("p-1")));
|
|
26
|
+
|
|
27
|
+
// claude api_error with NO ids → key = (api_error, None) — Option must hold None.
|
|
28
|
+
let api_err_noids = [serde_json::json!(
|
|
29
|
+
{"type":"system","subtype":"api_error","level":"error"}
|
|
30
|
+
)];
|
|
31
|
+
let f = read_fault_facts(&api_err_noids, Provider::Claude);
|
|
32
|
+
assert_eq!(f.len(), 1);
|
|
33
|
+
assert_eq!(f[0].signature, Signature::new("api_error"));
|
|
34
|
+
assert_eq!(f[0].turn_id, None, "api_error with no ids dedups on (api_error, None)");
|
|
35
|
+
|
|
36
|
+
// codex turn_failed → signature=turn_failed, turn_id=ct4, kind=failed.
|
|
37
|
+
let codex_failed = [serde_json::json!(
|
|
38
|
+
{"jsonrpc":"2.0","method":"turn/completed","params":{"turn":{"id":"ct4","status":"failed"}}}
|
|
39
|
+
)];
|
|
40
|
+
let f = read_fault_facts(&codex_failed, Provider::Codex);
|
|
41
|
+
assert_eq!(f.len(), 1);
|
|
42
|
+
assert_eq!(f[0].signature, Signature::new("turn_failed"));
|
|
43
|
+
assert_eq!(f[0].turn_id, Some(TurnId::new("ct4")));
|
|
44
|
+
assert_eq!(f[0].kind, FactKind::Failed);
|
|
45
|
+
|
|
46
|
+
// codex approval → signature=approval_required, turn_id=ct5, kind=approval.
|
|
47
|
+
let codex_approval = [serde_json::json!(
|
|
48
|
+
{"jsonrpc":"2.0","method":"session/requestApproval","params":{"turnId":"ct5"}}
|
|
49
|
+
)];
|
|
50
|
+
let f = read_fault_facts(&codex_approval, Provider::Codex);
|
|
51
|
+
assert_eq!(f.len(), 1);
|
|
52
|
+
assert_eq!(f[0].signature, Signature::new("approval_required"));
|
|
53
|
+
assert_eq!(f[0].turn_id, Some(TurnId::new("ct5")));
|
|
54
|
+
assert_eq!(f[0].kind, FactKind::Approval);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- (e) session capture payload + bug-085 fallback (confidence=low) ----
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn capture_session_id_success_payload_fields() {
|
|
61
|
+
// claude.py:73-106 success → CapturedSession high confidence, fs_watch,
|
|
62
|
+
// session_id Some, rollout_path Some. Drive the real fn against a temp cwd
|
|
63
|
+
// that contains a discoverable transcript; assert ALL 5 typed fields.
|
|
64
|
+
let dir = std::env::temp_dir().join(format!(
|
|
65
|
+
"ta-cap-success-{}",
|
|
66
|
+
std::process::id()
|
|
67
|
+
));
|
|
68
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
69
|
+
let transcript = dir.join("session.jsonl");
|
|
70
|
+
let _ = std::fs::write(
|
|
71
|
+
&transcript,
|
|
72
|
+
r#"{"type":"user","sessionId":"sess-1","cwd":"PLACEHOLDER","message":{"content":"hi"}}"#,
|
|
73
|
+
);
|
|
74
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
75
|
+
let cs = adapter
|
|
76
|
+
.capture_session_id("agentX", &dir, 3)
|
|
77
|
+
.expect("capture ok")
|
|
78
|
+
.expect("transcript found → Some");
|
|
79
|
+
assert_eq!(cs.captured_via, CaptureVia::FsWatch);
|
|
80
|
+
assert_eq!(cs.attribution_confidence, Confidence::High);
|
|
81
|
+
assert!(cs.session_id.is_some(), "found transcript yields a session_id");
|
|
82
|
+
assert!(cs.rollout_path.is_some(), "found transcript yields a rollout_path");
|
|
83
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn capture_session_id_bug085_compatible_api_fallback_is_low_confidence_none_session() {
|
|
88
|
+
// probe(claude fallback): compatible_api transcript w/o a session_id →
|
|
89
|
+
// session_id=None, captured_via=fs_mtime_fallback, confidence=low,
|
|
90
|
+
// rollout_path SET. The half-state is LEGAL and must not panic (bug-085).
|
|
91
|
+
let dir = std::env::temp_dir().join(format!(
|
|
92
|
+
"ta-cap-fallback-{}",
|
|
93
|
+
std::process::id()
|
|
94
|
+
));
|
|
95
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
96
|
+
let transcript = dir.join("nosession.jsonl");
|
|
97
|
+
let _ = std::fs::write(&transcript, r#"{"type":"user","message":{"content":"hi"}}"#);
|
|
98
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
99
|
+
let cs = adapter
|
|
100
|
+
.capture_session_id("agentX", &dir, 3)
|
|
101
|
+
.expect("capture ok")
|
|
102
|
+
.expect("fallback transcript → Some half-state");
|
|
103
|
+
assert_eq!(cs.session_id, None, "bug-085: fallback session_id is None");
|
|
104
|
+
assert_eq!(cs.captured_via, CaptureVia::FsMtimeFallback);
|
|
105
|
+
assert_eq!(cs.attribution_confidence, Confidence::Low);
|
|
106
|
+
assert!(cs.rollout_path.is_some(), "fallback still pins rollout_path");
|
|
107
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn capture_session_id_no_match_returns_none() {
|
|
112
|
+
// claude.py:111-113 deadline with no match → Ok(None), explicitly NOT Err.
|
|
113
|
+
let empty = std::env::temp_dir().join(format!("ta-cap-empty-{}", std::process::id()));
|
|
114
|
+
let _ = std::fs::create_dir_all(&empty);
|
|
115
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
116
|
+
let res = adapter.capture_session_id("agentX", &empty, 0);
|
|
117
|
+
// ProviderError 非 PartialEq(携 Io)→ 用 matches! 而非 ==。
|
|
118
|
+
assert!(matches!(res, Ok(None)), "no transcript + timeout 0 → Ok(None), never Err");
|
|
119
|
+
let _ = std::fs::remove_dir_all(&empty);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- resume / fork / build_command / caps — command-build core ----
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn claude_caps_resume_and_fork_and_native_mcp() {
|
|
126
|
+
// doc §59 + claude.py: claude supports resume + fork (static cap true,
|
|
127
|
+
// runtime auth-gated) + native --mcp-config; does NOT write a global settings file.
|
|
128
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
129
|
+
let caps = adapter.caps();
|
|
130
|
+
assert_eq!(
|
|
131
|
+
caps,
|
|
132
|
+
ProviderCaps {
|
|
133
|
+
resume: true,
|
|
134
|
+
fork: true,
|
|
135
|
+
native_mcp_config: true,
|
|
136
|
+
writes_global_settings: false,
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn gemini_writes_global_settings_cap() {
|
|
143
|
+
// gemini.py:40-78 — install_mcp writes ~/.gemini/settings.json →
|
|
144
|
+
// caps.writes_global_settings=true, native_mcp_config=false.
|
|
145
|
+
let adapter = get_adapter(Provider::GeminiCli);
|
|
146
|
+
let caps = adapter.caps();
|
|
147
|
+
assert!(caps.writes_global_settings, "gemini writes a global settings file");
|
|
148
|
+
assert!(!caps.native_mcp_config, "gemini has no native --mcp-config flag");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn claude_fork_blocked_under_compatible_api() {
|
|
153
|
+
// claude.py:54 supports_session_fork == (auth_mode != compatible_api).
|
|
154
|
+
// fork under CompatibleApi must Err (never silently empty) — capability path.
|
|
155
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
156
|
+
let sid = SessionId::new("src-1");
|
|
157
|
+
let res = adapter.fork(Some(&sid), AuthMode::CompatibleApi, None);
|
|
158
|
+
assert!(
|
|
159
|
+
matches!(
|
|
160
|
+
res,
|
|
161
|
+
Err(ProviderError::CapabilityUnsupported(_)) | Err(ProviderError::ResumeUnavailable(_))
|
|
162
|
+
),
|
|
163
|
+
"fork under compatible_api must Err, got {res:?}"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn build_resume_command_without_session_id_is_resume_unavailable() {
|
|
169
|
+
// claude.py:41-42 — no session_id → ResumeUnavailable, never a bare crash.
|
|
170
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
171
|
+
let res = adapter.build_resume_command(None, AuthMode::Subscription, None);
|
|
172
|
+
assert!(
|
|
173
|
+
matches!(res, Err(ProviderError::ResumeUnavailable(_))),
|
|
174
|
+
"resume without session_id must be ResumeUnavailable, got {res:?}"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn session_is_resumable_none_session_is_false_not_panic() {
|
|
180
|
+
// claude.py:116-118 — session_id falsy → not resumable; bug-085 None穿透.
|
|
181
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
182
|
+
let res = adapter.session_is_resumable(None, AuthMode::CompatibleApi);
|
|
183
|
+
assert!(matches!(res, Ok(false)), "None session → Ok(false), never panic (bug-085)");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// true iff `needle` (e.g. ["--model","opus"]) appears as a contiguous run in `hay`.
|
|
187
|
+
fn argv_contains_adjacent(hay: &[String], needle: &[&str]) -> bool {
|
|
188
|
+
if needle.is_empty() {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
hay.windows(needle.len())
|
|
192
|
+
.any(|w| w.iter().zip(needle).all(|(a, b)| a == b))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn build_command_includes_model_and_system_prompt() {
|
|
197
|
+
// claude.py:175-199 — _base_command appends --model <m> + --append-system-prompt <p>;
|
|
198
|
+
// --strict-mcp-config only appears when mcp_config is present. With mcp_config=None,
|
|
199
|
+
// assert both flag pairs present adjacently AND --strict-mcp-config ABSENT.
|
|
200
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
201
|
+
let argv = adapter
|
|
202
|
+
.build_command(AuthMode::Subscription, None, Some("be helpful"), Some("opus"))
|
|
203
|
+
.expect("build_command ok");
|
|
204
|
+
assert!(
|
|
205
|
+
argv_contains_adjacent(&argv, &["--model", "opus"]),
|
|
206
|
+
"argv must contain `--model opus` adjacency: {argv:?}"
|
|
207
|
+
);
|
|
208
|
+
assert!(
|
|
209
|
+
argv_contains_adjacent(&argv, &["--append-system-prompt", "be helpful"]),
|
|
210
|
+
"argv must contain `--append-system-prompt 'be helpful'`: {argv:?}"
|
|
211
|
+
);
|
|
212
|
+
assert!(
|
|
213
|
+
!argv.iter().any(|a| a == "--strict-mcp-config"),
|
|
214
|
+
"no mcp_config → --strict-mcp-config must be absent: {argv:?}"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn unsupported_provider_capability_error_message_shape() {
|
|
220
|
+
// unsupported.py:31 ProviderCapabilityError — placeholder providers reject
|
|
221
|
+
// on call. The skeleton Provider enum has no Copilot/Opencode variant, so
|
|
222
|
+
// the unsupported path is exercised through ProviderError::CapabilityUnsupported
|
|
223
|
+
// construction (message contract) here; full plug dispatch deferred.
|
|
224
|
+
let e = ProviderError::CapabilityUnsupported("opencode:start".to_string());
|
|
225
|
+
assert_eq!(
|
|
226
|
+
e.to_string(),
|
|
227
|
+
"provider capability unsupported: opencode:start"
|
|
228
|
+
);
|
|
229
|
+
let e2 = ProviderError::ResumeUnavailable("claude resume requires session_id".to_string());
|
|
230
|
+
assert_eq!(
|
|
231
|
+
e2.to_string(),
|
|
232
|
+
"resume unavailable: claude resume requires session_id"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
237
|
+
// Lock the CORRECT Python v0.2.11 behavior the strengthened contracts missed.
|
|
238
|
+
// Golden re-probed via /tmp/probe_p2_provider.py vs team-agent-public @ 439bef8.
|
|
239
|
+
|