@team-agent/installer 0.5.65 → 0.5.68
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 +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +6 -0
- package/crates/team-agent/src/cli/adapters.rs +6 -1
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/mod.rs +2 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler/tests.rs +23 -20
- package/crates/team-agent/src/compiler.rs +84 -5
- package/crates/team-agent/src/conpty/backend.rs +16 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +1080 -58
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +209 -43
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -15
- package/crates/team-agent/src/lifecycle/types.rs +61 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/model/spec.rs +129 -6
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +13 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1083 -75
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +265 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
|
@@ -468,12 +468,52 @@ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
|
|
|
468
468
|
|
|
469
469
|
#[test]
|
|
470
470
|
fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
471
|
+
#[cfg(unix)]
|
|
472
|
+
const CHILD: &str = "RB11_F01_NATURAL_PROBE_FAILURE_CHILD";
|
|
473
|
+
#[cfg(unix)]
|
|
474
|
+
const TEST: &str =
|
|
475
|
+
"cli::tests::shutdown_kill_plan::repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth";
|
|
476
|
+
|
|
477
|
+
#[cfg(unix)]
|
|
478
|
+
if std::env::var_os(CHILD).is_none() {
|
|
479
|
+
let path = tmp_shutdown_workspace("rb11-f01-empty-path").join("bin");
|
|
480
|
+
std::fs::create_dir_all(&path).unwrap();
|
|
481
|
+
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
482
|
+
.args(["--exact", TEST, "--nocapture", "--test-threads=1"])
|
|
483
|
+
.env(CHILD, "1")
|
|
484
|
+
.env("PATH", path)
|
|
485
|
+
.output()
|
|
486
|
+
.unwrap();
|
|
487
|
+
assert!(
|
|
488
|
+
output.status.success(),
|
|
489
|
+
"directed child failed: status={} stdout={} stderr={}",
|
|
490
|
+
output.status,
|
|
491
|
+
String::from_utf8_lossy(&output.stdout),
|
|
492
|
+
String::from_utf8_lossy(&output.stderr)
|
|
493
|
+
);
|
|
494
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
495
|
+
assert!(
|
|
496
|
+
stdout.contains("running 1 test")
|
|
497
|
+
&& stdout.contains(&format!("test {TEST} ..."))
|
|
498
|
+
&& stdout.contains("RB11_F01_CHILD_SUCCESS"),
|
|
499
|
+
"directed child omitted success marker: stdout={} stderr={}",
|
|
500
|
+
stdout,
|
|
501
|
+
String::from_utf8_lossy(&output.stderr)
|
|
502
|
+
);
|
|
503
|
+
println!("RB11_F01_DIRECTED_CHILD_OUTPUT\n{stdout}");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#[cfg(unix)]
|
|
508
|
+
let expected_probe_failure = std::env::var_os(CHILD).is_some();
|
|
509
|
+
#[cfg(not(unix))]
|
|
510
|
+
let expected_probe_failure = false;
|
|
471
511
|
let ws = tmp_shutdown_workspace("owned-loop-no-growth");
|
|
472
512
|
let sockets = (0..20)
|
|
473
513
|
.map(|idx| ws.join(format!("owned-loop-{idx}.sock")))
|
|
474
514
|
.collect::<Vec<_>>();
|
|
475
515
|
let starting = sockets.iter().filter(|path| path.exists()).count();
|
|
476
|
-
for socket in
|
|
516
|
+
for (iteration, socket) in sockets.iter().enumerate() {
|
|
477
517
|
std::fs::write(socket, b"socket").unwrap();
|
|
478
518
|
crate::state::persist::save_runtime_state(
|
|
479
519
|
&ws,
|
|
@@ -487,20 +527,251 @@ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
|
487
527
|
}),
|
|
488
528
|
)
|
|
489
529
|
.unwrap();
|
|
490
|
-
let
|
|
530
|
+
let before_len = crate::event_log::EventLog::new(&ws)
|
|
531
|
+
.tail(0)
|
|
532
|
+
.expect("read shutdown event boundary")
|
|
533
|
+
.len();
|
|
534
|
+
let result = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
491
535
|
&ws,
|
|
492
536
|
true,
|
|
493
537
|
None,
|
|
494
538
|
&CleanShutdownTransport::new(),
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
|
|
539
|
+
);
|
|
540
|
+
let out = match &result {
|
|
541
|
+
Ok(out) if !shutdown_result_is_degraded(out) => out,
|
|
542
|
+
Ok(out) if expected_probe_failure => {
|
|
543
|
+
let diagnostic =
|
|
544
|
+
shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
|
|
545
|
+
assert!(
|
|
546
|
+
shutdown_result_is_degraded(out),
|
|
547
|
+
"directed probe-failure child unexpectedly clean: {diagnostic}"
|
|
548
|
+
);
|
|
549
|
+
assert_eq!(
|
|
550
|
+
out["ok"],
|
|
551
|
+
json!(false),
|
|
552
|
+
"probe failure must remain degraded: {out}"
|
|
553
|
+
);
|
|
554
|
+
if iteration == 0 {
|
|
555
|
+
println!("RB11_F01_FIRST_FAILURE {diagnostic}");
|
|
556
|
+
}
|
|
557
|
+
out
|
|
558
|
+
}
|
|
559
|
+
Ok(out) => {
|
|
560
|
+
let diagnostic =
|
|
561
|
+
shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
|
|
562
|
+
panic!("unexpected degraded shutdown: {diagnostic}");
|
|
563
|
+
}
|
|
564
|
+
Err(error) => {
|
|
565
|
+
let diagnostic =
|
|
566
|
+
shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
|
|
567
|
+
panic!("shutdown invocation failed: {error}; {diagnostic}");
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
if !expected_probe_failure {
|
|
571
|
+
assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
|
|
572
|
+
}
|
|
498
573
|
}
|
|
499
574
|
let ending = sockets.iter().filter(|path| path.exists()).count();
|
|
500
575
|
assert_eq!(
|
|
501
576
|
ending, starting,
|
|
502
577
|
"owned socket files must not grow across loops"
|
|
503
578
|
);
|
|
579
|
+
#[cfg(unix)]
|
|
580
|
+
if expected_probe_failure {
|
|
581
|
+
println!("RB11_F01_CHILD_SUCCESS iterations={}", sockets.len());
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn shutdown_result_is_degraded(out: &serde_json::Value) -> bool {
|
|
586
|
+
out["ok"] != json!(true)
|
|
587
|
+
|| out["probe_degraded"] == json!(true)
|
|
588
|
+
|| out["verification_degraded"] == json!(true)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fn shutdown_invocation_diagnostic(
|
|
592
|
+
ws: &Path,
|
|
593
|
+
before_len: usize,
|
|
594
|
+
iteration: usize,
|
|
595
|
+
socket: &Path,
|
|
596
|
+
result: &Result<serde_json::Value, crate::cli::CliError>,
|
|
597
|
+
) -> String {
|
|
598
|
+
let result_summary = match result {
|
|
599
|
+
Ok(out) => format!("report={out}"),
|
|
600
|
+
Err(error) => format!("error={error}"),
|
|
601
|
+
};
|
|
602
|
+
let events = crate::event_log::EventLog::new(ws)
|
|
603
|
+
.tail(0)
|
|
604
|
+
.unwrap_or_else(|error| {
|
|
605
|
+
panic!(
|
|
606
|
+
"shutdown diagnostic event read failed: iteration={iteration} socket={} \
|
|
607
|
+
{result_summary}; error={error}",
|
|
608
|
+
socket.display()
|
|
609
|
+
)
|
|
610
|
+
});
|
|
611
|
+
let appended = events.get(before_len..).unwrap_or_else(|| {
|
|
612
|
+
panic!(
|
|
613
|
+
"shutdown diagnostic boundary invalid: iteration={iteration} socket={} \
|
|
614
|
+
{result_summary}; before_len={before_len} event_len={}",
|
|
615
|
+
socket.display(),
|
|
616
|
+
events.len()
|
|
617
|
+
)
|
|
618
|
+
});
|
|
619
|
+
fn event_name(event: &serde_json::Value) -> Option<&str> {
|
|
620
|
+
event["event"].as_str()
|
|
621
|
+
}
|
|
622
|
+
let starts = appended
|
|
623
|
+
.iter()
|
|
624
|
+
.enumerate()
|
|
625
|
+
.filter(|(_, event)| event_name(event) == Some("lifecycle.shutdown.started"))
|
|
626
|
+
.map(|(index, _)| index)
|
|
627
|
+
.collect::<Vec<_>>();
|
|
628
|
+
let terminals = appended
|
|
629
|
+
.iter()
|
|
630
|
+
.enumerate()
|
|
631
|
+
.filter(|(_, event)| event_name(event) == Some("lifecycle.shutdown"))
|
|
632
|
+
.map(|(index, _)| index)
|
|
633
|
+
.collect::<Vec<_>>();
|
|
634
|
+
if starts.len() != 1 || terminals.len() != 1 || starts[0] >= terminals[0] {
|
|
635
|
+
panic!(
|
|
636
|
+
"shutdown diagnostic markers invalid: iteration={iteration} socket={} \
|
|
637
|
+
{result_summary}; appended={appended:?}",
|
|
638
|
+
socket.display()
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
let terminal = &appended[terminals[0]];
|
|
642
|
+
let expected_socket = socket.to_string_lossy();
|
|
643
|
+
if terminal["owned_endpoint"].as_str() != Some(expected_socket.as_ref()) {
|
|
644
|
+
panic!(
|
|
645
|
+
"shutdown diagnostic endpoint mismatch: iteration={iteration} socket={} \
|
|
646
|
+
{result_summary}; terminal={terminal}",
|
|
647
|
+
socket.display()
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
let bounded = &appended[starts[0]..=terminals[0]];
|
|
651
|
+
let probe_failures = bounded
|
|
652
|
+
.iter()
|
|
653
|
+
.filter(|event| event_name(event) == Some("shutdown.process_probe_failed"))
|
|
654
|
+
.cloned()
|
|
655
|
+
.collect::<Vec<_>>();
|
|
656
|
+
if probe_failures.is_empty() {
|
|
657
|
+
panic!(
|
|
658
|
+
"shutdown diagnostic missing process-probe failure: iteration={iteration} \
|
|
659
|
+
socket={} {result_summary}; bounded={bounded:?}",
|
|
660
|
+
socket.display()
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
for event in &probe_failures {
|
|
664
|
+
let phase = event["phase"].as_str();
|
|
665
|
+
if event["probe"].as_str() != Some("ps_table")
|
|
666
|
+
|| !matches!(phase, Some("entry" | "residual_round" | "post_verify"))
|
|
667
|
+
|| !matches!(
|
|
668
|
+
event["error"].as_str(),
|
|
669
|
+
Some(error)
|
|
670
|
+
if !error.trim().is_empty()
|
|
671
|
+
&& !error.contains("exited with status")
|
|
672
|
+
&& (error.contains("No such file") || error.contains("os error 2"))
|
|
673
|
+
)
|
|
674
|
+
{
|
|
675
|
+
panic!(
|
|
676
|
+
"shutdown diagnostic probe failure invalid: iteration={iteration} \
|
|
677
|
+
socket={} {result_summary}; event={event}",
|
|
678
|
+
socket.display()
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
format!(
|
|
683
|
+
"iteration={iteration} socket={} {result_summary}; probe_failures={probe_failures:?}",
|
|
684
|
+
socket.display()
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#[cfg(unix)]
|
|
689
|
+
#[test]
|
|
690
|
+
fn rb07_probe_failure_diagnostic_is_iteration_bound() {
|
|
691
|
+
const CHILD: &str = "RB07_PROBE_FAILURE_DIAGNOSTIC_CHILD";
|
|
692
|
+
const TEST: &str =
|
|
693
|
+
"cli::tests::shutdown_kill_plan::rb07_probe_failure_diagnostic_is_iteration_bound";
|
|
694
|
+
if std::env::var_os(CHILD).is_none() {
|
|
695
|
+
let path = tmp_shutdown_workspace("rb07-empty-path").join("bin");
|
|
696
|
+
std::fs::create_dir_all(&path).unwrap();
|
|
697
|
+
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
698
|
+
.args(["--exact", TEST, "--nocapture"])
|
|
699
|
+
.env(CHILD, "1")
|
|
700
|
+
.env("PATH", path)
|
|
701
|
+
.output()
|
|
702
|
+
.unwrap();
|
|
703
|
+
assert!(
|
|
704
|
+
output.status.success(),
|
|
705
|
+
"directed child failed: status={} stdout={} stderr={}",
|
|
706
|
+
output.status,
|
|
707
|
+
String::from_utf8_lossy(&output.stdout),
|
|
708
|
+
String::from_utf8_lossy(&output.stderr)
|
|
709
|
+
);
|
|
710
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
711
|
+
assert!(
|
|
712
|
+
stdout.contains("running 1 test")
|
|
713
|
+
&& stdout.contains(&format!("test {TEST} ..."))
|
|
714
|
+
&& stdout.contains("RB07_CHILD_SUCCESS"),
|
|
715
|
+
"directed child omitted success marker: stdout={} stderr={}",
|
|
716
|
+
stdout,
|
|
717
|
+
String::from_utf8_lossy(&output.stderr)
|
|
718
|
+
);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let ws = tmp_shutdown_workspace("rb07-child");
|
|
723
|
+
let socket = ws.join("owned.sock");
|
|
724
|
+
crate::state::persist::save_runtime_state(
|
|
725
|
+
&ws,
|
|
726
|
+
&json!({
|
|
727
|
+
"session_name": "team-rb07-child",
|
|
728
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
729
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
730
|
+
"tmux_socket_source": "workspace",
|
|
731
|
+
"is_external_leader": false,
|
|
732
|
+
"agents": {}
|
|
733
|
+
}),
|
|
734
|
+
)
|
|
735
|
+
.unwrap();
|
|
736
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
737
|
+
event_log
|
|
738
|
+
.write(
|
|
739
|
+
"shutdown.process_probe_failed",
|
|
740
|
+
json!({
|
|
741
|
+
"phase": "entry",
|
|
742
|
+
"probe": "ps_table",
|
|
743
|
+
"error": "rb07-stale-decoy"
|
|
744
|
+
}),
|
|
745
|
+
)
|
|
746
|
+
.unwrap();
|
|
747
|
+
let before_len = event_log.tail(0).unwrap().len();
|
|
748
|
+
let result = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
749
|
+
&ws,
|
|
750
|
+
true,
|
|
751
|
+
None,
|
|
752
|
+
&CleanShutdownTransport::new(),
|
|
753
|
+
);
|
|
754
|
+
let diagnostic = shutdown_invocation_diagnostic(&ws, before_len, 0, &socket, &result);
|
|
755
|
+
let failures = match result {
|
|
756
|
+
Ok(out) => {
|
|
757
|
+
assert!(
|
|
758
|
+
shutdown_result_is_degraded(&out),
|
|
759
|
+
"child must force ps failure: {out}"
|
|
760
|
+
);
|
|
761
|
+
let text = diagnostic;
|
|
762
|
+
assert!(
|
|
763
|
+
!text.contains("rb07-stale-decoy"),
|
|
764
|
+
"stale event leaked: {text}"
|
|
765
|
+
);
|
|
766
|
+
text
|
|
767
|
+
}
|
|
768
|
+
Err(error) => panic!("child shutdown unexpectedly returned Err: {error}; {diagnostic}"),
|
|
769
|
+
};
|
|
770
|
+
assert!(
|
|
771
|
+
failures.contains("probe_failures=["),
|
|
772
|
+
"missing bounded failures: {failures}"
|
|
773
|
+
);
|
|
774
|
+
println!("RB07_CHILD_SUCCESS {failures}");
|
|
504
775
|
}
|
|
505
776
|
|
|
506
777
|
fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
|
|
@@ -521,6 +792,7 @@ struct CleanShutdownTransport {
|
|
|
521
792
|
kill_server_called: Mutex<bool>,
|
|
522
793
|
probe_timeout_kind: Option<&'static str>,
|
|
523
794
|
targets_persist_after_kill: bool,
|
|
795
|
+
kill_session_error: Option<String>,
|
|
524
796
|
// E49 (0.3.24 P0, shutdown kills leader CLI): record per-pane / per-window /
|
|
525
797
|
// per-session kill targets so RED contracts can assert (a) the leader pane is
|
|
526
798
|
// never killed and (b) worker panes ARE killed via the new per-pane path.
|
|
@@ -537,6 +809,7 @@ impl CleanShutdownTransport {
|
|
|
537
809
|
kill_server_called: Mutex::new(false),
|
|
538
810
|
probe_timeout_kind: None,
|
|
539
811
|
targets_persist_after_kill: false,
|
|
812
|
+
kill_session_error: None,
|
|
540
813
|
killed_panes: Mutex::new(Vec::new()),
|
|
541
814
|
killed_window_targets: Mutex::new(Vec::new()),
|
|
542
815
|
killed_sessions: Mutex::new(Vec::new()),
|
|
@@ -562,6 +835,11 @@ impl CleanShutdownTransport {
|
|
|
562
835
|
self
|
|
563
836
|
}
|
|
564
837
|
|
|
838
|
+
fn with_kill_session_error(mut self, detail: impl Into<String>) -> Self {
|
|
839
|
+
self.kill_session_error = Some(detail.into());
|
|
840
|
+
self
|
|
841
|
+
}
|
|
842
|
+
|
|
565
843
|
/// E49 (0.3.24 P0): observed per-pane kill targets, in call order.
|
|
566
844
|
fn killed_panes(&self) -> Vec<String> {
|
|
567
845
|
self.killed_panes.lock().unwrap().clone()
|
|
@@ -675,7 +953,19 @@ impl Transport for CleanShutdownTransport {
|
|
|
675
953
|
.unwrap()
|
|
676
954
|
.push(session.as_str().to_string());
|
|
677
955
|
*self.session_present.lock().unwrap() = false;
|
|
678
|
-
|
|
956
|
+
match self.kill_session_error.as_deref() {
|
|
957
|
+
Some(detail) => Err(TransportError::Subprocess {
|
|
958
|
+
argv: vec![
|
|
959
|
+
"tmux".to_string(),
|
|
960
|
+
"kill-session".to_string(),
|
|
961
|
+
"-t".to_string(),
|
|
962
|
+
session.as_str().to_string(),
|
|
963
|
+
],
|
|
964
|
+
code: Some(1),
|
|
965
|
+
stderr: detail.to_string(),
|
|
966
|
+
}),
|
|
967
|
+
None => Ok(()),
|
|
968
|
+
}
|
|
679
969
|
}
|
|
680
970
|
|
|
681
971
|
fn kill_server(&self) -> Result<(), TransportError> {
|
|
@@ -1132,6 +1422,8 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
|
|
|
1132
1422
|
])
|
|
1133
1423
|
.with_targets_persist_after_kill();
|
|
1134
1424
|
|
|
1425
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
1426
|
+
let event_start = event_log.tail(0).expect("events before shutdown").len();
|
|
1135
1427
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1136
1428
|
.expect("shutdown should complete");
|
|
1137
1429
|
|
|
@@ -1142,9 +1434,98 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
|
|
|
1142
1434
|
the team session — leader pane lives elsewhere so this is safe. Got \
|
|
1143
1435
|
transport killed_sessions={killed_sessions:?}"
|
|
1144
1436
|
);
|
|
1437
|
+
if out["ok"] != json!(true) {
|
|
1438
|
+
let events = event_log.tail(0).expect("events after shutdown");
|
|
1439
|
+
let invocation_events = events
|
|
1440
|
+
.into_iter()
|
|
1441
|
+
.skip(event_start)
|
|
1442
|
+
.take(64)
|
|
1443
|
+
.collect::<Vec<_>>();
|
|
1444
|
+
panic!(
|
|
1445
|
+
"E49 external-leader shutdown returned a non-clean outcome: workspace tag=e49-external-leader-still-kills, expected session=team-external, shutdown report={out}, killed_sessions={killed_sessions:?}, bounded invocation events={invocation_events:?}"
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1145
1448
|
assert_eq!(out["ok"], json!(true));
|
|
1146
1449
|
}
|
|
1147
1450
|
|
|
1451
|
+
#[test]
|
|
1452
|
+
fn e49_external_leader_non_clean_diagnostic_is_bounded_and_still_kills() {
|
|
1453
|
+
let ws = tmp_shutdown_workspace("e49-external-leader-non-clean");
|
|
1454
|
+
crate::state::persist::save_runtime_state(
|
|
1455
|
+
&ws,
|
|
1456
|
+
&json!({
|
|
1457
|
+
"session_name": "team-external",
|
|
1458
|
+
"is_external_leader": true,
|
|
1459
|
+
"leader_receiver": {"pane_id": "%leaderelsewhere"},
|
|
1460
|
+
"agents": {
|
|
1461
|
+
"w1": {"status": "running", "provider": "codex", "window": "w1", "pane_id": "%w1"}
|
|
1462
|
+
}
|
|
1463
|
+
}),
|
|
1464
|
+
)
|
|
1465
|
+
.unwrap();
|
|
1466
|
+
let transport = CleanShutdownTransport::new()
|
|
1467
|
+
.with_targets(vec![PaneInfo {
|
|
1468
|
+
pane_id: PaneId::new("%w1"),
|
|
1469
|
+
session: SessionName::new("team-external"),
|
|
1470
|
+
window_index: Some(0),
|
|
1471
|
+
window_name: Some(WindowName::new("w1")),
|
|
1472
|
+
pane_index: Some(0),
|
|
1473
|
+
tty: None,
|
|
1474
|
+
current_command: Some("codex".to_string()),
|
|
1475
|
+
current_path: None,
|
|
1476
|
+
active: true,
|
|
1477
|
+
pane_pid: None,
|
|
1478
|
+
leader_env: BTreeMap::new(),
|
|
1479
|
+
}])
|
|
1480
|
+
.with_kill_session_error("deterministic E49 kill-session failure");
|
|
1481
|
+
|
|
1482
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
1483
|
+
let event_start = event_log.tail(0).expect("events before shutdown").len();
|
|
1484
|
+
let diagnostic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
1485
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1486
|
+
.expect("shutdown should complete");
|
|
1487
|
+
let killed_sessions = transport.killed_sessions_observed();
|
|
1488
|
+
assert!(
|
|
1489
|
+
killed_sessions.iter().any(|s| s == "team-external"),
|
|
1490
|
+
concat!(
|
|
1491
|
+
"E49 non-clean path must still attempt kill_session(team-external); ",
|
|
1492
|
+
"got killed_sessions={:?}"
|
|
1493
|
+
),
|
|
1494
|
+
killed_sessions
|
|
1495
|
+
);
|
|
1496
|
+
if out["ok"] != json!(true) {
|
|
1497
|
+
let events = event_log.tail(0).expect("events after shutdown");
|
|
1498
|
+
let invocation_events = events
|
|
1499
|
+
.into_iter()
|
|
1500
|
+
.skip(event_start)
|
|
1501
|
+
.take(64)
|
|
1502
|
+
.collect::<Vec<_>>();
|
|
1503
|
+
panic!(
|
|
1504
|
+
"E49 external-leader shutdown returned a non-clean outcome: workspace tag=e49-external-leader-non-clean, expected session=team-external, shutdown report={out}, killed_sessions={killed_sessions:?}, bounded invocation events={invocation_events:?}"
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
}))
|
|
1508
|
+
.expect_err("deterministic kill-session error must exercise the diagnostic path");
|
|
1509
|
+
let diagnostic = diagnostic
|
|
1510
|
+
.downcast_ref::<String>()
|
|
1511
|
+
.cloned()
|
|
1512
|
+
.or_else(|| diagnostic.downcast_ref::<&str>().map(ToString::to_string))
|
|
1513
|
+
.expect("diagnostic panic should carry text");
|
|
1514
|
+
let bounded_events = diagnostic
|
|
1515
|
+
.split("bounded invocation events=")
|
|
1516
|
+
.nth(1)
|
|
1517
|
+
.expect("diagnostic should include bounded invocation events");
|
|
1518
|
+
let event_count = bounded_events.matches("\"event\":").count();
|
|
1519
|
+
assert!(
|
|
1520
|
+
event_count > 0,
|
|
1521
|
+
"diagnostic should include invocation events"
|
|
1522
|
+
);
|
|
1523
|
+
assert!(
|
|
1524
|
+
event_count <= 64,
|
|
1525
|
+
"diagnostic must be bounded to 64 invocation events, got {event_count}"
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1148
1529
|
/// E49 regression guard: managed topology where the leader anchor pane is NOT
|
|
1149
1530
|
/// in `state.session_name` (e.g. a stale state from an aborted launch where
|
|
1150
1531
|
/// only worker panes exist in the session). The pre-fix behaviour (kill_session)
|
|
@@ -227,13 +227,12 @@ fn rm039_stat001_status_resolves_active_team_when_root_team_key_missing() {
|
|
|
227
227
|
#[test]
|
|
228
228
|
fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
229
229
|
// cmd_status json branch (detail=false) delegates status_port::status(compact=true).
|
|
230
|
-
//
|
|
231
|
-
//
|
|
230
|
+
// Compact slim is the original 7 keys plus `grok_slot` and
|
|
231
|
+
// `grok_identity_slot_ok`. `ok` is command success (always true here).
|
|
232
232
|
let ws = seed_status_workspace();
|
|
233
233
|
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
234
234
|
.expect("seeded fixture status should project a value");
|
|
235
235
|
let obj = v.as_object().expect("--json status is a dict");
|
|
236
|
-
// Exactly these 7 keys, no more.
|
|
237
236
|
let expected: std::collections::BTreeSet<&str> = [
|
|
238
237
|
"ok",
|
|
239
238
|
"team",
|
|
@@ -242,6 +241,8 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
|
242
241
|
"ready",
|
|
243
242
|
"not_ready",
|
|
244
243
|
"agents",
|
|
244
|
+
"grok_slot",
|
|
245
|
+
"grok_identity_slot_ok",
|
|
245
246
|
]
|
|
246
247
|
.iter()
|
|
247
248
|
.copied()
|
|
@@ -249,7 +250,30 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
|
249
250
|
let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
|
|
250
251
|
assert_eq!(
|
|
251
252
|
actual, expected,
|
|
252
|
-
"
|
|
253
|
+
"compact must expose the original 7 keys plus grok_slot and grok_identity_slot_ok (9); got {actual:?}"
|
|
254
|
+
);
|
|
255
|
+
let grok_slot = obj
|
|
256
|
+
.get("grok_slot")
|
|
257
|
+
.and_then(serde_json::Value::as_object)
|
|
258
|
+
.expect("compact must carry grok_slot");
|
|
259
|
+
let grok_ok = grok_slot
|
|
260
|
+
.get("consistent")
|
|
261
|
+
.and_then(serde_json::Value::as_bool)
|
|
262
|
+
== Some(true)
|
|
263
|
+
&& grok_slot
|
|
264
|
+
.get("readable")
|
|
265
|
+
.and_then(serde_json::Value::as_bool)
|
|
266
|
+
== Some(true);
|
|
267
|
+
assert_eq!(
|
|
268
|
+
obj.get("ok").and_then(serde_json::Value::as_bool),
|
|
269
|
+
Some(true),
|
|
270
|
+
"compact ok is command success and must stay true; grok_slot={grok_slot:?}"
|
|
271
|
+
);
|
|
272
|
+
assert_eq!(
|
|
273
|
+
obj.get("grok_identity_slot_ok")
|
|
274
|
+
.and_then(serde_json::Value::as_bool),
|
|
275
|
+
Some(grok_ok),
|
|
276
|
+
"grok_identity_slot_ok must follow readable∧consistent; grok_slot={grok_slot:?}"
|
|
253
277
|
);
|
|
254
278
|
// Diagnostic keys must NOT leak into the default compact payload.
|
|
255
279
|
for forbidden in [
|
|
@@ -282,6 +306,44 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
|
282
306
|
let _ = std::fs::remove_dir_all(&ws);
|
|
283
307
|
}
|
|
284
308
|
|
|
309
|
+
#[test]
|
|
310
|
+
fn status_compact_reports_identity_slot_health_on_grok_identity_slot_ok() {
|
|
311
|
+
// 已废除的行为:旧实现看不见 grok 身份槽。此墓碑留下槽健康断言。
|
|
312
|
+
// 2026-08-18 裁定:`ok` 是命令成功标志,成功投影必须恒真;身份槽健康
|
|
313
|
+
// 走顶层 `grok_identity_slot_ok`,不再重载 `ok`。
|
|
314
|
+
// 2026-08-18 身份串台:judge-g 的产出从 developer-d61 的通道报出并占掉
|
|
315
|
+
// 后者 exactly-once report_result。那时若完全不报槽健康才是一句谎。
|
|
316
|
+
let ws = seed_status_workspace();
|
|
317
|
+
let grok = ws.join(".grok");
|
|
318
|
+
std::fs::create_dir_all(&grok).unwrap();
|
|
319
|
+
std::fs::write(
|
|
320
|
+
grok.join("config.toml"),
|
|
321
|
+
"[mcp_servers.team_orchestrator.env]\nTEAM_AGENT_ID = \"impostor-seat\"\n",
|
|
322
|
+
)
|
|
323
|
+
.unwrap();
|
|
324
|
+
|
|
325
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
326
|
+
.expect("mismatched grok slot must still project compact status");
|
|
327
|
+
assert_eq!(
|
|
328
|
+
v.get("ok").and_then(serde_json::Value::as_bool),
|
|
329
|
+
Some(true),
|
|
330
|
+
"successful status must keep ok=true; slot health is a sibling key; got {v}"
|
|
331
|
+
);
|
|
332
|
+
assert_eq!(
|
|
333
|
+
v.get("grok_identity_slot_ok")
|
|
334
|
+
.and_then(serde_json::Value::as_bool),
|
|
335
|
+
Some(false),
|
|
336
|
+
"identity-slot mismatch must force grok_identity_slot_ok=false; got {v}"
|
|
337
|
+
);
|
|
338
|
+
assert_eq!(
|
|
339
|
+
v.pointer("/grok_slot/consistent")
|
|
340
|
+
.and_then(serde_json::Value::as_bool),
|
|
341
|
+
Some(false),
|
|
342
|
+
"fixture must actually be a slot mismatch; got {v}"
|
|
343
|
+
);
|
|
344
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
345
|
+
}
|
|
346
|
+
|
|
285
347
|
#[test]
|
|
286
348
|
fn cmd_status_human_appends_harness_reminder() {
|
|
287
349
|
let ws = seed_status_workspace();
|
|
@@ -617,8 +679,14 @@ fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
|
|
|
617
679
|
"channel:",
|
|
618
680
|
"reminder:",
|
|
619
681
|
] {
|
|
682
|
+
// 字段级匹配,⛔ 不用裸 contains:人类输出以 " " 连接 `"{key}: {value}"`
|
|
683
|
+
// (send/presentation.rs 的 `parts.join(" ")` / `send_human_field`),
|
|
684
|
+
// 所以一个字段只可能出现在行首或某个空格之后。裸子串会让
|
|
685
|
+
// `turn_verification:` 误命中名单里的 `verification:`(该键由 ab2134c0
|
|
686
|
+
// 有意加进人类输出名单)——那是判据误伤,不是产品缺陷。
|
|
687
|
+
let present = text.starts_with(hidden) || text.contains(&format!(" {hidden}"));
|
|
620
688
|
assert!(
|
|
621
|
-
!
|
|
689
|
+
!present,
|
|
622
690
|
"default send output should hide {hidden} unless needed; got {text}"
|
|
623
691
|
);
|
|
624
692
|
}
|
|
@@ -913,10 +981,8 @@ fn run_send_legacy_task_flag_is_internalized_before_persistence() {
|
|
|
913
981
|
ExitCode::Ok,
|
|
914
982
|
"legacy --task is sunset-noticed and ignored; public send persists without caller binding"
|
|
915
983
|
);
|
|
916
|
-
let connection =
|
|
917
|
-
ws.join(".team").join("runtime").join("team.db")
|
|
918
|
-
)
|
|
919
|
-
.unwrap();
|
|
984
|
+
let connection =
|
|
985
|
+
rusqlite::Connection::open(ws.join(".team").join("runtime").join("team.db")).unwrap();
|
|
920
986
|
let task_id: Option<String> = connection
|
|
921
987
|
.query_row(
|
|
922
988
|
"SELECT task_id FROM messages WHERE content = 'go' ORDER BY rowid DESC LIMIT 1",
|
|
@@ -924,7 +990,10 @@ fn run_send_legacy_task_flag_is_internalized_before_persistence() {
|
|
|
924
990
|
|row| row.get(0),
|
|
925
991
|
)
|
|
926
992
|
.unwrap();
|
|
927
|
-
assert_eq!(
|
|
993
|
+
assert_eq!(
|
|
994
|
+
task_id, None,
|
|
995
|
+
"caller-supplied task ids must not reach storage"
|
|
996
|
+
);
|
|
928
997
|
let _ = std::fs::remove_dir_all(&ws);
|
|
929
998
|
}
|
|
930
999
|
|
|
@@ -10,7 +10,7 @@ pub enum CommunicationMode {
|
|
|
10
10
|
|
|
11
11
|
const LEADER_CENTRIC_RUNTIME_CONTRACT: &str = r#"# Team Agent communication contract: leader_centric
|
|
12
12
|
|
|
13
|
-
- Progress, blockers, questions:
|
|
13
|
+
- Progress, blockers, questions: {send_message}(to='leader', content='...')
|
|
14
14
|
|
|
15
15
|
When you receive a message from the leader or a teammate, you MUST respond
|
|
16
16
|
through MCP tools. Writing a reply in your terminal does nothing — the sender
|
|
@@ -39,10 +39,12 @@ impl CommunicationMode {
|
|
|
39
39
|
.find(|mode| mode.as_str() == value)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
pub
|
|
42
|
+
pub fn runtime_contract(self, send_message: &str) -> String {
|
|
43
43
|
match self {
|
|
44
|
-
Self::LeaderCentric =>
|
|
45
|
-
|
|
44
|
+
Self::LeaderCentric => {
|
|
45
|
+
LEADER_CENTRIC_RUNTIME_CONTRACT.replace("{send_message}", send_message)
|
|
46
|
+
}
|
|
47
|
+
Self::Orchestrated => ORCHESTRATED_RUNTIME_CONTRACT.to_string(),
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
}
|