@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.
Files changed (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -0,0 +1,562 @@
1
+ //! ---
2
+ //! purpose: 把 leader 手写的三件判据夹具迁成 cargo test 能跑到的测试基建
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: assert_probe_red
6
+ //! what: 跑 probe,要求修复前非零且不是 exit 2;已绿则 Fail,exit 2 保持 Unjudgeable
7
+ //! - name: assert_probe_two_sided
8
+ //! what: 先要求坏状态红,再跑 make_green,再要求好状态绿;缺任一半则 Fail,exit 2 不折态
9
+ //! - name: check_brief
10
+ //! what: lint 任务书是否含既有四条硬条款;读不到则 Unjudgeable,缺条款则 Fail
11
+ //! depends:
12
+ //! - sh(只用来跑调用方提供的 probe / make_green 脚本)
13
+ //! boundary:
14
+ //! - 不删除、不改写 .team/probes 下原三件;去留由审查席裁定
15
+ //! - 不发明第四个退出码;超预算与不适用都保持 exit 2(不可判),不折进 0/1
16
+ //! - 不写产品路径判据,只夹住「判据本身有没有分辨力」
17
+ //! - ⛔ 「工具自己坏了」不许叫 Unjudgeable:sh 起不来 / 收不到输出 / 子进程被信号打死
18
+ //! 一律走 GateVerdict::ToolFailure,与探针自报 exit 2 是两个值
19
+ //! maturity: wired
20
+ //! ---
21
+
22
+ use std::cell::{Cell, RefCell};
23
+ use std::fs;
24
+ use std::path::{Path, PathBuf};
25
+ use std::process::{Command, Stdio};
26
+ use std::sync::atomic::{AtomicU64, Ordering};
27
+ use std::time::{SystemTime, UNIX_EPOCH};
28
+
29
+ static SEQ: AtomicU64 = AtomicU64::new(0);
30
+
31
+ /// 夹具四态里实际会出口的三值(超预算与不适用都走 Unjudgeable,exit 2,与原脚本一致),
32
+ /// 外加一个**不属于四态**的出口:ToolFailure —— 量具自己坏了,不是被测对象不可判。
33
+ #[derive(Debug, Clone, PartialEq, Eq)]
34
+ enum GateVerdict {
35
+ Pass,
36
+ Fail,
37
+ Unjudgeable,
38
+ /// 量具坏了:sh 起不来 / 输出收不回来 / 子进程被信号打死。
39
+ /// ⛔ 绝不塌进 Unjudgeable——那样「探针真报不可判」与「探针根本没跑」就没有分辨力。
40
+ ToolFailure(ShFailure),
41
+ }
42
+
43
+ /// run_sh 的失败分类。Debug 打出来即为一眼可辨的失败原因。
44
+ #[derive(Debug, Clone, PartialEq, Eq)]
45
+ enum ShFailure {
46
+ /// 起不了 sh(CI 上没有 sh / PATH 不对 / 权限)——⛔ 与探针 exit 2 无关。
47
+ Spawn { script: String, error: String },
48
+ /// sh 起来了但收不回 stdout/stderr。
49
+ OutputCapture { script: String, error: String },
50
+ /// 子进程没有退出码:被信号打死(unix 下带信号号)。
51
+ KilledBySignal { script: String, signal: Option<i32> },
52
+ }
53
+
54
+ impl GateVerdict {
55
+ fn from_probe_exit(code: i32) -> ProbeOutcome {
56
+ match code {
57
+ 0 => ProbeOutcome::Green,
58
+ 2 => ProbeOutcome::Unjudgeable,
59
+ _ => ProbeOutcome::Red,
60
+ }
61
+ }
62
+ }
63
+
64
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65
+ enum ProbeOutcome {
66
+ Green,
67
+ Red,
68
+ Unjudgeable,
69
+ }
70
+
71
+ /// ---
72
+ /// purpose: 因果性夹具——probe 必须在修复前判红
73
+ /// params:
74
+ /// probe: 被夹的判据脚本路径
75
+ /// returns: Pass=见到红;Fail=修复前已绿;Unjudgeable=缺文件或 probe 自报 exit 2;
76
+ /// ToolFailure=sh 根本没跑起来/输出丢了/被信号打死(量具坏,非不可判)
77
+ /// ---
78
+ fn assert_probe_red(probe: &Path) -> GateVerdict {
79
+ if !probe.is_file() {
80
+ return GateVerdict::Unjudgeable;
81
+ }
82
+ let log = probe
83
+ .parent()
84
+ .unwrap_or_else(|| Path::new("."))
85
+ .join("RED-EVIDENCE.log");
86
+ let _ = fs::write(
87
+ &log,
88
+ format!("#PROBE {}\n#---- 原始输出 ----\n", probe.display()),
89
+ );
90
+ match run_sh(probe, &log) {
91
+ Err(failure) => GateVerdict::ToolFailure(failure),
92
+ Ok(0) => GateVerdict::Fail,
93
+ Ok(2) => GateVerdict::Unjudgeable,
94
+ Ok(_) => GateVerdict::Pass,
95
+ }
96
+ }
97
+
98
+ /// ---
99
+ /// purpose: 两头夹住——坏状态必须红,好状态必须绿
100
+ /// params:
101
+ /// probe: 被夹的判据脚本
102
+ /// make_green: 只许动隔离夹具的阳性对照
103
+ /// returns: 两半都成立才 Pass;因果性或可满足性缺一则 Fail;缺脚本 / 对照失败 / exit 2 为 Unjudgeable;
104
+ /// 任一次 sh 没跑起来则 ToolFailure(量具坏,非不可判)
105
+ /// ---
106
+ fn assert_probe_two_sided(probe: &Path, make_green: &Path) -> GateVerdict {
107
+ if !probe.is_file() || !make_green.is_file() {
108
+ return GateVerdict::Unjudgeable;
109
+ }
110
+ let log = probe
111
+ .parent()
112
+ .unwrap_or_else(|| Path::new("."))
113
+ .join("TWO-SIDED-EVIDENCE.log");
114
+ let _ = fs::write(
115
+ &log,
116
+ format!(
117
+ "#PROBE {}\n#MAKEGREEN {}\n",
118
+ probe.display(),
119
+ make_green.display()
120
+ ),
121
+ );
122
+
123
+ match run_sh(probe, &log) {
124
+ Err(failure) => return GateVerdict::ToolFailure(failure),
125
+ Ok(0) => return GateVerdict::Fail,
126
+ Ok(2) => return GateVerdict::Unjudgeable,
127
+ Ok(_) => {}
128
+ }
129
+
130
+ match run_sh(make_green, &log) {
131
+ Err(failure) => return GateVerdict::ToolFailure(failure),
132
+ Ok(0) => {}
133
+ // 阳性对照自己没做成(脚本跑了但没成功)——这是真·不可判,不是量具坏。
134
+ Ok(_) => return GateVerdict::Unjudgeable,
135
+ }
136
+
137
+ match run_sh(probe, &log) {
138
+ Err(failure) => GateVerdict::ToolFailure(failure),
139
+ Ok(0) => GateVerdict::Pass,
140
+ Ok(2) => GateVerdict::Unjudgeable,
141
+ Ok(_) => GateVerdict::Fail,
142
+ }
143
+ }
144
+
145
+ const BRIEF_REQUIRED: &[(&str, &[&str])] = &[
146
+ (
147
+ "report_result 不传 task_id",
148
+ &["不要传 task_id", "不传 task_id", "留空走框架默认归属"],
149
+ ),
150
+ (
151
+ "禁止改判据",
152
+ &["不许改判据", "不得修改判据", "判据由", "不许动判据"],
153
+ ),
154
+ ("产物落地路径", &[".team/nodes/"]),
155
+ (
156
+ "验收判据是行为自证",
157
+ &["行为自证", "退出码", "expected_exit_code"],
158
+ ),
159
+ ];
160
+
161
+ /// ---
162
+ /// purpose: 任务书结构 lint,只挡「缺了必失败」的已知条款
163
+ /// params:
164
+ /// task_md: TASK.md 路径
165
+ /// returns: 条款齐全 Pass;缺条款或过短 Fail;读不到 Unjudgeable
166
+ /// ---
167
+ fn check_brief(task_md: &Path) -> GateVerdict {
168
+ let text = match fs::read_to_string(task_md) {
169
+ Ok(t) => t,
170
+ Err(_) => return GateVerdict::Unjudgeable,
171
+ };
172
+ if text.trim().len() < 200 {
173
+ return GateVerdict::Fail;
174
+ }
175
+ let missing = BRIEF_REQUIRED
176
+ .iter()
177
+ .any(|(_name, keys)| !keys.iter().any(|k| text.contains(k)));
178
+ if missing {
179
+ GateVerdict::Fail
180
+ } else {
181
+ GateVerdict::Pass
182
+ }
183
+ }
184
+
185
+ /// ---
186
+ /// purpose: 跑一个 sh 脚本并把失败原因分类带回来
187
+ /// returns: Ok(退出码);Err(ShFailure) 区分 起不来 / 收不到输出 / 被信号打死
188
+ /// ---
189
+ /// ⛔ 不许再用 `.ok()?` 把 io::Error 吞成 None——那会让「量具坏」与「exit 2」同值。
190
+ /// 拆成 spawn + wait_with_output 两步,正是为了让前两类可分。
191
+ fn run_sh(script: &Path, log: &Path) -> Result<i32, ShFailure> {
192
+ let name = script.display().to_string();
193
+ let child = Command::new(shell_for_this_call())
194
+ .arg(script)
195
+ // 钉死子 shell 自己的 PATH:它内部再解析裸名外部命令(如 touch)时不能继承
196
+ // 父进程当次可能已被并发测试改写的 PATH(绝对路径只保证这一次 spawn 本身不查 PATH)。
197
+ .env("PATH", "/usr/bin:/bin")
198
+ .stdout(Stdio::piped())
199
+ .stderr(Stdio::piped())
200
+ .spawn()
201
+ .map_err(|error| ShFailure::Spawn {
202
+ script: name.clone(),
203
+ error: error.to_string(),
204
+ })?;
205
+ let output = child
206
+ .wait_with_output()
207
+ .map_err(|error| ShFailure::OutputCapture {
208
+ script: name.clone(),
209
+ error: error.to_string(),
210
+ })?;
211
+ let mut buf = fs::read(log).unwrap_or_default();
212
+ buf.extend_from_slice(&output.stdout);
213
+ buf.extend_from_slice(&output.stderr);
214
+ let _ = fs::write(log, buf);
215
+ output
216
+ .status
217
+ .code()
218
+ .ok_or_else(|| ShFailure::KilledBySignal {
219
+ script: name,
220
+ signal: termination_signal(&output.status),
221
+ })
222
+ }
223
+
224
+ /// 一个**绝对路径**的不存在解释器。绝对路径 ⇒ execvp 不查 PATH,必然 ENOENT。
225
+ /// ⛔ 不用「unset PATH」那一招:unix 下 PATH 未设时 execvp 回落 confstr(_CS_PATH),
226
+ /// 仍能找到 /bin/sh,破坏是无效的。
227
+ const UNSPAWNABLE_SHELL: &str = "/nonexistent-gate-fixtures/no-such-shell";
228
+
229
+ thread_local! {
230
+ /// 只在**本线程**生效的量具破坏开关:列出「第几次 run_sh 调用该用坏解释器」。
231
+ /// 线程局部 ⇒ 不碰 PATH、不碰任何进程级环境,绝不外溢到并发跑的别的测试。
232
+ static BREAK_SH_ON_CALLS: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
233
+ static SH_CALL_SEQ: Cell<usize> = const { Cell::new(0) };
234
+ }
235
+
236
+ fn shell_for_this_call() -> &'static str {
237
+ let n = SH_CALL_SEQ.with(|seq| {
238
+ let n = seq.get();
239
+ seq.set(n + 1);
240
+ n
241
+ });
242
+ if BREAK_SH_ON_CALLS.with(|calls| calls.borrow().contains(&n)) {
243
+ UNSPAWNABLE_SHELL
244
+ } else {
245
+ // 绝对路径 ⇒ execve 不查 PATH:不与并发跑的、会原地改写进程级 PATH 的其它测试赛跑。
246
+ "/bin/sh"
247
+ }
248
+ }
249
+
250
+ /// 在本线程内令第 `calls` 次 run_sh 调用的解释器起不来,跑完立刻复位。
251
+ /// 用于在**健康环境**下主动造出「量具失效」,不依赖运行环境恰好坏掉。
252
+ fn with_broken_sh_on<T>(calls: &[usize], body: impl FnOnce() -> T) -> T {
253
+ SH_CALL_SEQ.with(|seq| seq.set(0));
254
+ BREAK_SH_ON_CALLS.with(|c| *c.borrow_mut() = calls.to_vec());
255
+ let out = body();
256
+ BREAK_SH_ON_CALLS.with(|c| c.borrow_mut().clear());
257
+ SH_CALL_SEQ.with(|seq| seq.set(0));
258
+ out
259
+ }
260
+
261
+ #[cfg(unix)]
262
+ fn termination_signal(status: &std::process::ExitStatus) -> Option<i32> {
263
+ use std::os::unix::process::ExitStatusExt;
264
+ status.signal()
265
+ }
266
+
267
+ #[cfg(not(unix))]
268
+ fn termination_signal(_status: &std::process::ExitStatus) -> Option<i32> {
269
+ None
270
+ }
271
+
272
+ fn scratch_dir() -> PathBuf {
273
+ // scratch 根必须可移植:CI(ubuntu) 上 `/Volumes/...` 不存在且 `/` 不可写,
274
+ // create_dir_all 直接 errno 13。⛔ 不硬编码任何绝对路径;默认走标准临时目录
275
+ // (`std::env::temp_dir()`,尊重 TMPDIR),`TEAM_AGENT_TEST_TMP` 仍可覆盖。
276
+ let base = std::env::var_os("TEAM_AGENT_TEST_TMP")
277
+ .map(PathBuf::from)
278
+ .unwrap_or_else(std::env::temp_dir);
279
+ let n = SEQ.fetch_add(1, Ordering::Relaxed);
280
+ let nanos = SystemTime::now()
281
+ .duration_since(UNIX_EPOCH)
282
+ .map(|d| d.as_nanos())
283
+ .unwrap_or(0);
284
+ let dir = base.join(format!(
285
+ "gate-fixtures-{}-{}-{}",
286
+ std::process::id(),
287
+ nanos,
288
+ n
289
+ ));
290
+ let _ = fs::remove_dir_all(&dir);
291
+ fs::create_dir_all(&dir).expect("create gate-fixtures scratch");
292
+ dir
293
+ }
294
+
295
+ fn write_sh(dir: &Path, name: &str, body: &str) -> PathBuf {
296
+ let path = dir.join(name);
297
+ fs::write(&path, format!("#!/bin/sh\nset -u\n{body}\n")).expect("write fixture script");
298
+ path
299
+ }
300
+
301
+ fn valid_brief() -> String {
302
+ // 远超 200 字符,四条硬条款各出现至少一处。故意弄坏测试会删掉其中一条。
303
+ format!(
304
+ "本任务书给下一棒执行。产物必须落在 .team/nodes/gate-governance/ 下。\n\
305
+ 调 report_result 不要传 task_id,留空走框架默认归属。\n\
306
+ 不许改判据。探针与夹具实现席都不得修改。\n\
307
+ 验收必须是行为自证:看退出码 / expected_exit_code,不要看席位自报。\n\
308
+ 填充段落:{}。\n",
309
+ "约束坐标与隔离环境说明。".repeat(8)
310
+ )
311
+ }
312
+
313
+ #[test]
314
+ fn assert_probe_red_passes_on_a_genuinely_red_probe() {
315
+ let dir = scratch_dir();
316
+ let probe = write_sh(&dir, "probe.sh", "exit 1");
317
+ assert_eq!(assert_probe_red(&probe), GateVerdict::Pass);
318
+ }
319
+
320
+ #[test]
321
+ fn assert_probe_red_fails_when_subject_is_already_green() {
322
+ let dir = scratch_dir();
323
+ let good = write_sh(&dir, "ok.sh", "exit 1");
324
+ assert_eq!(assert_probe_red(&good), GateVerdict::Pass);
325
+ let broken = write_sh(&dir, "already-green.sh", "exit 0");
326
+ assert_eq!(assert_probe_red(&broken), GateVerdict::Fail);
327
+ }
328
+
329
+ #[test]
330
+ fn assert_probe_red_keeps_exit2_unjudgeable_not_red_or_green() {
331
+ let dir = scratch_dir();
332
+ let probe = write_sh(&dir, "probe.sh", "exit 2");
333
+ let v = assert_probe_red(&probe);
334
+ assert_eq!(v, GateVerdict::Unjudgeable);
335
+ assert_ne!(v, GateVerdict::Fail);
336
+ assert_ne!(v, GateVerdict::Pass);
337
+ }
338
+
339
+ #[test]
340
+ fn assert_probe_red_missing_probe_is_unjudgeable() {
341
+ let dir = scratch_dir();
342
+ assert_eq!(
343
+ assert_probe_red(&dir.join("nope.sh")),
344
+ GateVerdict::Unjudgeable
345
+ );
346
+ }
347
+
348
+ #[test]
349
+ fn assert_probe_two_sided_passes_when_both_halves_hold() {
350
+ let dir = scratch_dir();
351
+ let flag = dir.join("fixed");
352
+ let probe = write_sh(
353
+ &dir,
354
+ "probe.sh",
355
+ &format!(
356
+ "if [ -f '{}' ]; then exit 0; else exit 1; fi",
357
+ flag.display()
358
+ ),
359
+ );
360
+ let mk = write_sh(
361
+ &dir,
362
+ "make_green.sh",
363
+ &format!("touch '{}'", flag.display()),
364
+ );
365
+ assert_eq!(assert_probe_two_sided(&probe, &mk), GateVerdict::Pass);
366
+ }
367
+
368
+ #[test]
369
+ fn assert_probe_two_sided_fails_when_probe_is_green_before_fix() {
370
+ let dir = scratch_dir();
371
+ let probe = write_sh(&dir, "probe.sh", "exit 0");
372
+ let mk = write_sh(&dir, "make_green.sh", "exit 0");
373
+ assert_eq!(assert_probe_two_sided(&probe, &mk), GateVerdict::Fail);
374
+ }
375
+
376
+ #[test]
377
+ fn assert_probe_two_sided_fails_when_probe_can_never_go_green() {
378
+ let dir = scratch_dir();
379
+ let probe = write_sh(&dir, "probe.sh", "exit 1");
380
+ let mk = write_sh(&dir, "make_green.sh", "exit 0");
381
+ assert_eq!(assert_probe_two_sided(&probe, &mk), GateVerdict::Fail);
382
+ }
383
+
384
+ #[test]
385
+ fn assert_probe_two_sided_keeps_exit2_unjudgeable_on_either_half() {
386
+ let dir = scratch_dir();
387
+ let always2 = write_sh(&dir, "u.sh", "exit 2");
388
+ let mk = write_sh(&dir, "make_green.sh", "exit 0");
389
+ let v1 = assert_probe_two_sided(&always2, &mk);
390
+ assert_eq!(v1, GateVerdict::Unjudgeable);
391
+ assert_ne!(v1, GateVerdict::Fail);
392
+ assert_ne!(v1, GateVerdict::Pass);
393
+
394
+ let flag = dir.join("fixed");
395
+ let probe = write_sh(
396
+ &dir,
397
+ "probe.sh",
398
+ &format!(
399
+ "if [ -f '{}' ]; then exit 2; else exit 1; fi",
400
+ flag.display()
401
+ ),
402
+ );
403
+ let mk2 = write_sh(&dir, "mk2.sh", &format!("touch '{}'", flag.display()));
404
+ let v2 = assert_probe_two_sided(&probe, &mk2);
405
+ assert_eq!(v2, GateVerdict::Unjudgeable);
406
+ }
407
+
408
+ #[test]
409
+ fn assert_probe_two_sided_make_green_failure_is_unjudgeable() {
410
+ let dir = scratch_dir();
411
+ let probe = write_sh(&dir, "probe.sh", "exit 1");
412
+ let mk = write_sh(&dir, "make_green.sh", "exit 1");
413
+ assert_eq!(
414
+ assert_probe_two_sided(&probe, &mk),
415
+ GateVerdict::Unjudgeable
416
+ );
417
+ }
418
+
419
+ #[test]
420
+ fn check_brief_passes_when_all_required_clauses_present() {
421
+ let dir = scratch_dir();
422
+ let path = dir.join("TASK.md");
423
+ fs::write(&path, valid_brief()).unwrap();
424
+ assert_eq!(check_brief(&path), GateVerdict::Pass);
425
+ }
426
+
427
+ #[test]
428
+ fn check_brief_fails_when_one_required_clause_is_removed() {
429
+ let dir = scratch_dir();
430
+ let good = dir.join("good.md");
431
+ fs::write(&good, valid_brief()).unwrap();
432
+ assert_eq!(check_brief(&good), GateVerdict::Pass);
433
+
434
+ let broken = valid_brief()
435
+ .replace("不许改判据", "")
436
+ .replace("不得修改判据", "")
437
+ .replace("判据由", "")
438
+ .replace("不许动判据", "");
439
+ let path = dir.join("TASK.md");
440
+ fs::write(&path, broken).unwrap();
441
+ assert_eq!(check_brief(&path), GateVerdict::Fail);
442
+ }
443
+
444
+ #[test]
445
+ fn check_brief_unreadable_is_unjudgeable_not_fail() {
446
+ let dir = scratch_dir();
447
+ let v = check_brief(&dir.join("missing.md"));
448
+ assert_eq!(v, GateVerdict::Unjudgeable);
449
+ assert_ne!(v, GateVerdict::Fail);
450
+ assert_ne!(v, GateVerdict::Pass);
451
+ }
452
+
453
+ #[test]
454
+ fn check_brief_too_short_is_fail() {
455
+ let dir = scratch_dir();
456
+ let path = dir.join("TASK.md");
457
+ fs::write(&path, "太短").unwrap();
458
+ assert_eq!(check_brief(&path), GateVerdict::Fail);
459
+ }
460
+
461
+ #[test]
462
+ fn four_state_probe_red_is_not_collapsed_to_boolean() {
463
+ let dir = scratch_dir();
464
+ let green = write_sh(&dir, "g.sh", "exit 0");
465
+ let red = write_sh(&dir, "r.sh", "exit 7");
466
+ let unj = write_sh(&dir, "u.sh", "exit 2");
467
+ assert_eq!(GateVerdict::from_probe_exit(0), ProbeOutcome::Green);
468
+ assert_eq!(GateVerdict::from_probe_exit(7), ProbeOutcome::Red);
469
+ assert_eq!(GateVerdict::from_probe_exit(2), ProbeOutcome::Unjudgeable);
470
+ assert_eq!(assert_probe_red(&green), GateVerdict::Fail);
471
+ assert_eq!(assert_probe_red(&red), GateVerdict::Pass);
472
+ assert_eq!(assert_probe_red(&unj), GateVerdict::Unjudgeable);
473
+ }
474
+
475
+ /// 齿:量具失效必须产生 ToolFailure,⛔ 不得塌回 Unjudgeable。
476
+ ///
477
+ /// 这条守的是 fea1e2fd 新立的不变量本身。没有它,把三处
478
+ /// `Err(f) => ToolFailure(f)` 改回 `Err(_) => Unjudgeable` 不会有任何测试报警,
479
+ /// 那三条刚封住的恒绿空壳就会原样复活。
480
+ ///
481
+ /// 破坏是**自己造的**(thread-local 把某一次 run_sh 的解释器换成绝对不存在的路径),
482
+ /// 因此在健康环境下也能跑、也必须绿;⛔ 不依赖运行环境恰好坏掉。
483
+ #[test]
484
+ fn tool_failure_is_never_collapsed_into_unjudgeable() {
485
+ let dir = scratch_dir();
486
+ let two = write_sh(&dir, "u.sh", "exit 2");
487
+ let red = write_sh(&dir, "r.sh", "exit 1");
488
+ let mk = write_sh(&dir, "make_green.sh", "exit 0");
489
+
490
+ // 阳性对照:同一个 exit 2 探针,sh 健康时是真·不可判。
491
+ let healthy = assert_probe_red(&two);
492
+ assert_eq!(
493
+ healthy,
494
+ GateVerdict::Unjudgeable,
495
+ "阳性对照失效:健康 sh 下 exit 2 必须是 Unjudgeable"
496
+ );
497
+
498
+ // 齿① assert_probe_red 的那次 run_sh。
499
+ let t1 = with_broken_sh_on(&[0], || assert_probe_red(&two));
500
+ println!("[齿① assert_probe_red ] {t1:?}");
501
+ assert_ne!(
502
+ t1,
503
+ GateVerdict::Unjudgeable,
504
+ "量具起不来被塌回 Unjudgeable:与探针自报 exit 2 无法区分。t1={t1:?}"
505
+ );
506
+ assert!(
507
+ matches!(t1, GateVerdict::ToolFailure(ShFailure::Spawn { .. })),
508
+ "量具起不来必须是 ToolFailure(Spawn)。t1={t1:?}"
509
+ );
510
+
511
+ // 齿② assert_probe_two_sided 第 1 次 run_sh(坏态探针)。
512
+ let t2 = with_broken_sh_on(&[0], || assert_probe_two_sided(&red, &mk));
513
+ println!("[齿② two_sided 坏态探针 ] {t2:?}");
514
+ assert_ne!(
515
+ t2,
516
+ GateVerdict::Unjudgeable,
517
+ "two_sided 第一次 run_sh 塌态。t2={t2:?}"
518
+ );
519
+ assert!(
520
+ matches!(t2, GateVerdict::ToolFailure(ShFailure::Spawn { .. })),
521
+ "t2={t2:?}"
522
+ );
523
+
524
+ // 齿③ 第 2 次 run_sh(阳性对照 make_green)——与「对照跑了但没成功」必须是两回事。
525
+ let t3 = with_broken_sh_on(&[1], || assert_probe_two_sided(&red, &mk));
526
+ println!("[齿③ two_sided make_green ] {t3:?}");
527
+ assert_ne!(
528
+ t3,
529
+ GateVerdict::Unjudgeable,
530
+ "对照脚本起不来 ≠ 对照没做成,⛔ 不许同值。t3={t3:?}"
531
+ );
532
+ assert!(
533
+ matches!(t3, GateVerdict::ToolFailure(ShFailure::Spawn { .. })),
534
+ "t3={t3:?}"
535
+ );
536
+
537
+ // 齿④ 第 3 次 run_sh(修好后复跑探针)。
538
+ let flag = dir.join("fixed");
539
+ let probe = write_sh(
540
+ &dir,
541
+ "probe.sh",
542
+ &format!(
543
+ "if [ -f '{}' ]; then exit 0; else exit 1; fi",
544
+ flag.display()
545
+ ),
546
+ );
547
+ let mk2 = write_sh(&dir, "mk2.sh", &format!("touch '{}'", flag.display()));
548
+ let t4 = with_broken_sh_on(&[2], || assert_probe_two_sided(&probe, &mk2));
549
+ println!("[齿④ two_sided 复跑探针 ] {t4:?}");
550
+ assert_ne!(t4, GateVerdict::Unjudgeable, "t4={t4:?}");
551
+ assert!(
552
+ matches!(t4, GateVerdict::ToolFailure(ShFailure::Spawn { .. })),
553
+ "t4={t4:?}"
554
+ );
555
+
556
+ // 破坏开关必须已复位:紧接着的健康调用不得再坏。
557
+ assert_eq!(
558
+ assert_probe_red(&two),
559
+ GateVerdict::Unjudgeable,
560
+ "with_broken_sh_on 没复位,破坏外溢"
561
+ );
562
+ }