@team-agent/installer 0.3.39 → 0.4.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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +7 -0
- package/crates/team-agent/src/cli/adapters.rs +8 -3
- package/crates/team-agent/src/cli/diagnose.rs +12 -3
- package/crates/team-agent/src/cli/emit.rs +1 -0
- package/crates/team-agent/src/cli/mod.rs +250 -9
- package/crates/team-agent/src/cli/send.rs +11 -2
- package/crates/team-agent/src/cli/status_port.rs +39 -4
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +189 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +189 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +5 -0
- package/crates/team-agent/src/coordinator/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +4 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +4 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +4 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +83 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +4 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +4 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +5 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +156 -0
- package/crates/team-agent/src/coordinator/tick.rs +126 -30
- package/crates/team-agent/src/{message_store.rs → db/message_store.rs} +6 -0
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/layout/mod.rs +7 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +312 -0
- package/crates/team-agent/src/layout/tmux_endpoint.rs +162 -0
- package/crates/team-agent/src/leader/start.rs +27 -1
- package/crates/team-agent/src/leader/tests/identity.rs +50 -0
- package/crates/team-agent/src/lib.rs +6 -2
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +32 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +32 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +51 -0
- package/crates/team-agent/src/lifecycle/launch.rs +34 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +143 -23
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +87 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +308 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +87 -22
- package/crates/team-agent/src/lifecycle/restart.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +6 -1
- package/crates/team-agent/src/lifecycle/tests/restart.rs +187 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +20 -1
- package/crates/team-agent/src/mcp_server/helpers.rs +72 -1
- package/crates/team-agent/src/mcp_server/tools.rs +29 -4
- package/crates/team-agent/src/provider/adapter.rs +31 -1
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +34 -0
- package/crates/team-agent/src/provider/classify.rs +138 -0
- package/crates/team-agent/src/provider/command.rs +71 -0
- package/crates/team-agent/src/provider/mod.rs +3 -0
- package/crates/team-agent/src/{session_capture.rs → provider/session/capture.rs} +139 -6
- package/crates/team-agent/src/provider/session/mod.rs +13 -0
- package/crates/team-agent/src/provider/session/resume.rs +417 -0
- package/crates/team-agent/src/provider/session_scan.rs +63 -0
- package/crates/team-agent/src/provider/types.rs +5 -0
- package/crates/team-agent/src/state/persist.rs +238 -0
- package/crates/team-agent/src/tmux_backend.rs +25 -0
- package/npm/install.mjs +27 -1
- package/package.json +4 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//! unit-7 (Stage 3) — provider session-scan boundary.
|
|
2
|
+
//!
|
|
3
|
+
//! Dedicated home for provider transcript/backing scanning concerns
|
|
4
|
+
//! (today co-located in adapter.rs around lines 511-546 and 969-1078).
|
|
5
|
+
//! Created as part of the unit-7 boundary establishment; full relocation
|
|
6
|
+
//! of the scanning fns is staged into follow-up commits so each diff
|
|
7
|
+
//! stays reviewable.
|
|
8
|
+
//!
|
|
9
|
+
//! See sibling [`crate::provider::session::capture`] for the existing
|
|
10
|
+
//! polling layer (moved under provider/session in unit-6).
|
|
11
|
+
|
|
12
|
+
/// Marker label for which scan strategy a provider uses. The Codex jsonl
|
|
13
|
+
/// scan and the Claude transcript scan share the same outer "decide what
|
|
14
|
+
/// to call" hook in adapter.rs; this enum exists to name the branch.
|
|
15
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
16
|
+
pub enum SessionScanStrategy {
|
|
17
|
+
/// Codex rollout jsonl scan (the existing
|
|
18
|
+
/// `agent_rollout_path`-based check).
|
|
19
|
+
CodexJsonl,
|
|
20
|
+
/// Claude transcript scan (per-cwd `.claude/projects/<cwd>/*.jsonl`).
|
|
21
|
+
ClaudeTranscript,
|
|
22
|
+
/// Copilot scan (different home; provider-specific layout).
|
|
23
|
+
CopilotHome,
|
|
24
|
+
/// Provider does not require a backing scan to decide resume — the
|
|
25
|
+
/// session-id alone is the authority.
|
|
26
|
+
NotRequired,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl SessionScanStrategy {
|
|
30
|
+
pub fn for_provider(provider: crate::provider::Provider) -> Self {
|
|
31
|
+
match provider {
|
|
32
|
+
crate::provider::Provider::Codex => Self::CodexJsonl,
|
|
33
|
+
crate::provider::Provider::Claude | crate::provider::Provider::ClaudeCode => {
|
|
34
|
+
Self::ClaudeTranscript
|
|
35
|
+
}
|
|
36
|
+
crate::provider::Provider::Copilot => Self::CopilotHome,
|
|
37
|
+
crate::provider::Provider::GeminiCli => Self::NotRequired,
|
|
38
|
+
crate::provider::Provider::Fake => Self::NotRequired,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[cfg(test)]
|
|
44
|
+
mod tests {
|
|
45
|
+
use super::*;
|
|
46
|
+
use crate::provider::Provider;
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn strategy_per_provider() {
|
|
50
|
+
assert_eq!(
|
|
51
|
+
SessionScanStrategy::for_provider(Provider::Codex),
|
|
52
|
+
SessionScanStrategy::CodexJsonl
|
|
53
|
+
);
|
|
54
|
+
assert_eq!(
|
|
55
|
+
SessionScanStrategy::for_provider(Provider::Claude),
|
|
56
|
+
SessionScanStrategy::ClaudeTranscript
|
|
57
|
+
);
|
|
58
|
+
assert_eq!(
|
|
59
|
+
SessionScanStrategy::for_provider(Provider::Fake),
|
|
60
|
+
SessionScanStrategy::NotRequired
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -342,6 +342,11 @@ pub struct ProviderCommandContext<'a> {
|
|
|
342
342
|
pub model: Option<&'a str>,
|
|
343
343
|
pub tools: &'a [&'a str],
|
|
344
344
|
pub profile_launch: Option<&'a ProviderProfileLaunch>,
|
|
345
|
+
/// Optional agent id, used as a launch-time display-name hint for
|
|
346
|
+
/// providers that accept `--name <name>` (Claude, Copilot). Codex has
|
|
347
|
+
/// no `--name` CLI flag (probe verdict 2026-06-22) and ignores this
|
|
348
|
+
/// field. None = legacy callers (the field is purely additive).
|
|
349
|
+
pub agent_id_hint: Option<&'a str>,
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -642,6 +642,72 @@ pub fn migrate_active_team_key(state: &mut Value) -> bool {
|
|
|
642
642
|
true
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
+
/// RM-039-STAT-001 second-round compat normalizer (architect verdict
|
|
646
|
+
/// 2026-06-22). When `active_team_key` names an existing `teams` entry
|
|
647
|
+
/// but root `team_key` is missing, set root `team_key = active_team_key`
|
|
648
|
+
/// (and mirror it into `teams[active_team_key].team_key` for callers
|
|
649
|
+
/// that read the team-scoped slot). This keeps the cascade in
|
|
650
|
+
/// `state::projection::team_state_key` honest: the first branch
|
|
651
|
+
/// (`state.team_key`) now matches what `active_team_key` says, so
|
|
652
|
+
/// coordinator tick's `save_team_scoped_state` writes activity to the
|
|
653
|
+
/// SAME teams entry the status selector later reads.
|
|
654
|
+
///
|
|
655
|
+
/// Narrow on purpose:
|
|
656
|
+
/// * Returns `false` (no-op) when root `team_key` already exists,
|
|
657
|
+
/// even if it disagrees with `active_team_key`. Conflict cases
|
|
658
|
+
/// stay observable; we don't silently rewrite.
|
|
659
|
+
/// * Returns `false` when `active_team_key` is null/empty.
|
|
660
|
+
/// * Returns `false` when `teams[active_team_key]` does not exist.
|
|
661
|
+
/// * Does NOT touch any other field (no generic deep merge).
|
|
662
|
+
///
|
|
663
|
+
/// Returns `true` if state was mutated and the caller should persist.
|
|
664
|
+
pub fn migrate_team_key_to_match_active_team(state: &mut Value) -> bool {
|
|
665
|
+
let Some(obj) = state.as_object() else {
|
|
666
|
+
return false;
|
|
667
|
+
};
|
|
668
|
+
let already_has_root = obj
|
|
669
|
+
.get("team_key")
|
|
670
|
+
.and_then(Value::as_str)
|
|
671
|
+
.is_some_and(|s| !s.is_empty());
|
|
672
|
+
if already_has_root {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
let active = match obj.get("active_team_key").and_then(Value::as_str) {
|
|
676
|
+
Some(s) if !s.is_empty() => s.to_string(),
|
|
677
|
+
_ => return false,
|
|
678
|
+
};
|
|
679
|
+
let teams_has_active = obj
|
|
680
|
+
.get("teams")
|
|
681
|
+
.and_then(Value::as_object)
|
|
682
|
+
.is_some_and(|teams| teams.contains_key(&active));
|
|
683
|
+
if !teams_has_active {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
let Some(obj_mut) = state.as_object_mut() else {
|
|
687
|
+
return false;
|
|
688
|
+
};
|
|
689
|
+
obj_mut.insert("team_key".to_string(), Value::String(active.clone()));
|
|
690
|
+
// Mirror inside teams[active] so any code reading the team-scoped
|
|
691
|
+
// slot sees the same canonical key. Skip if the entry isn't an
|
|
692
|
+
// object (defensive — should always be).
|
|
693
|
+
if let Some(team_entry) = obj_mut
|
|
694
|
+
.get_mut("teams")
|
|
695
|
+
.and_then(Value::as_object_mut)
|
|
696
|
+
.and_then(|teams| teams.get_mut(&active))
|
|
697
|
+
.and_then(Value::as_object_mut)
|
|
698
|
+
{
|
|
699
|
+
let needs_team_key = team_entry
|
|
700
|
+
.get("team_key")
|
|
701
|
+
.and_then(Value::as_str)
|
|
702
|
+
.map(|s| s != active)
|
|
703
|
+
.unwrap_or(true);
|
|
704
|
+
if needs_team_key {
|
|
705
|
+
team_entry.insert("team_key".to_string(), Value::String(active));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
true
|
|
709
|
+
}
|
|
710
|
+
|
|
645
711
|
pub fn load_runtime_state(workspace: &Path) -> Result<Value, StateError> {
|
|
646
712
|
let path = runtime_state_path(workspace);
|
|
647
713
|
if !path.exists() {
|
|
@@ -657,6 +723,13 @@ pub fn load_runtime_state(workspace: &Path) -> Result<Value, StateError> {
|
|
|
657
723
|
if migrate_active_team_key(&mut state) {
|
|
658
724
|
changed = true;
|
|
659
725
|
}
|
|
726
|
+
// RM-039-STAT-001 second-round compat (architect verdict 2026-06-22):
|
|
727
|
+
// for states written before the launch-side `team_key` fix landed,
|
|
728
|
+
// promote `active_team_key` into root `team_key` so coordinator tick
|
|
729
|
+
// and status selector agree on which `teams` entry to read/write.
|
|
730
|
+
if migrate_team_key_to_match_active_team(&mut state) {
|
|
731
|
+
changed = true;
|
|
732
|
+
}
|
|
660
733
|
if changed {
|
|
661
734
|
save_runtime_state(workspace, &state)?;
|
|
662
735
|
}
|
|
@@ -1037,4 +1110,169 @@ mod tests {
|
|
|
1037
1110
|
"deleted_agent_ids 豁免:被 remove 的 `gone` 不得被 preserve 复活;saved={saved}"
|
|
1038
1111
|
);
|
|
1039
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
/// RM-039-STAT-001 second-round regression (architect verdict
|
|
1115
|
+
/// 2026-06-22): `migrate_team_key_to_match_active_team` must
|
|
1116
|
+
/// promote `team_key = active_team_key` when root `team_key` is
|
|
1117
|
+
/// missing AND `teams[active_team_key]` exists, and mirror it into
|
|
1118
|
+
/// the team-scoped slot. Coordinator tick then writes to the same
|
|
1119
|
+
/// teams entry that status selector reads.
|
|
1120
|
+
#[test]
|
|
1121
|
+
fn migrate_team_key_to_match_active_team_promotes_when_missing() {
|
|
1122
|
+
let mut state = json!({
|
|
1123
|
+
"active_team_key": "rm039-status-working-891",
|
|
1124
|
+
"session_name": "team-rm039-status-working",
|
|
1125
|
+
"team_dir": "./.team/current",
|
|
1126
|
+
"teams": {
|
|
1127
|
+
"rm039-status-working-891": {
|
|
1128
|
+
"active_team_key": "rm039-status-working-891",
|
|
1129
|
+
"session_name": "team-rm039-status-working",
|
|
1130
|
+
"agents": {}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
let changed = migrate_team_key_to_match_active_team(&mut state);
|
|
1135
|
+
assert!(changed, "state must be mutated when team_key is missing");
|
|
1136
|
+
assert_eq!(
|
|
1137
|
+
state.get("team_key").and_then(Value::as_str),
|
|
1138
|
+
Some("rm039-status-working-891"),
|
|
1139
|
+
"root team_key must equal active_team_key after migration"
|
|
1140
|
+
);
|
|
1141
|
+
// mirrored into the team-scoped entry
|
|
1142
|
+
assert_eq!(
|
|
1143
|
+
state
|
|
1144
|
+
.pointer("/teams/rm039-status-working-891/team_key")
|
|
1145
|
+
.and_then(Value::as_str),
|
|
1146
|
+
Some("rm039-status-working-891"),
|
|
1147
|
+
"teams[active].team_key must be set as part of the migration"
|
|
1148
|
+
);
|
|
1149
|
+
// team_state_key cascade now agrees with active_team_key.
|
|
1150
|
+
assert_eq!(
|
|
1151
|
+
crate::state::projection::team_state_key(&state),
|
|
1152
|
+
"rm039-status-working-891",
|
|
1153
|
+
"team_state_key first branch must hit `state.team_key` and \
|
|
1154
|
+
return the canonical key"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/// Conflict cases stay observable: if `team_key` already exists,
|
|
1159
|
+
/// the migrator does NOT silently overwrite it, even when it
|
|
1160
|
+
/// disagrees with `active_team_key`. The user/team can resolve.
|
|
1161
|
+
#[test]
|
|
1162
|
+
fn migrate_team_key_to_match_active_team_is_noop_when_team_key_present() {
|
|
1163
|
+
let mut state = json!({
|
|
1164
|
+
"team_key": "explicit-key",
|
|
1165
|
+
"active_team_key": "different-active",
|
|
1166
|
+
"teams": {
|
|
1167
|
+
"different-active": {"agents": {}},
|
|
1168
|
+
"explicit-key": {"agents": {}}
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
let changed = migrate_team_key_to_match_active_team(&mut state);
|
|
1172
|
+
assert!(!changed, "existing team_key must NOT be overwritten");
|
|
1173
|
+
assert_eq!(
|
|
1174
|
+
state.get("team_key").and_then(Value::as_str),
|
|
1175
|
+
Some("explicit-key")
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/// Narrow: only promote when teams[active_team_key] actually exists.
|
|
1180
|
+
/// Otherwise we'd be claiming a key for a non-existent team slot.
|
|
1181
|
+
#[test]
|
|
1182
|
+
fn migrate_team_key_to_match_active_team_is_noop_when_active_team_entry_missing() {
|
|
1183
|
+
let mut state = json!({
|
|
1184
|
+
"active_team_key": "missing-team",
|
|
1185
|
+
"teams": {
|
|
1186
|
+
"other": {"agents": {}}
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
let changed = migrate_team_key_to_match_active_team(&mut state);
|
|
1190
|
+
assert!(
|
|
1191
|
+
!changed,
|
|
1192
|
+
"no migration when teams[active_team_key] does not exist"
|
|
1193
|
+
);
|
|
1194
|
+
assert!(state.get("team_key").is_none());
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/// Null / empty active_team_key is not actionable.
|
|
1198
|
+
#[test]
|
|
1199
|
+
fn migrate_team_key_to_match_active_team_is_noop_when_active_team_key_absent() {
|
|
1200
|
+
for active in [json!(null), json!(""), json!(serde_json::Value::Null)] {
|
|
1201
|
+
let mut state = json!({
|
|
1202
|
+
"active_team_key": active,
|
|
1203
|
+
"teams": {}
|
|
1204
|
+
});
|
|
1205
|
+
assert!(
|
|
1206
|
+
!migrate_team_key_to_match_active_team(&mut state),
|
|
1207
|
+
"no migration when active_team_key is null/empty"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/// RM-039-STAT-001 second-round end-to-end at the persistence layer
|
|
1213
|
+
/// (architect verdict 2026-06-22): loading a state file in the dirty
|
|
1214
|
+
/// shape (active_team_key disagrees with team_dir basename, root
|
|
1215
|
+
/// team_key absent) must persist the canonical `team_key` so the
|
|
1216
|
+
/// next `save_team_scoped_state` writes the team-scoped slot at
|
|
1217
|
+
/// `teams[active_team_key]`, not at `teams[team_dir_basename]`.
|
|
1218
|
+
/// This is the bridging contract that decides whether coordinator
|
|
1219
|
+
/// tick's activity write lands where the status selector later reads.
|
|
1220
|
+
#[test]
|
|
1221
|
+
fn load_runtime_state_pins_team_key_from_active_team_key_on_dirty_shape() {
|
|
1222
|
+
let ws = temp_ws();
|
|
1223
|
+
let active = "rm039-status-working-891";
|
|
1224
|
+
let raw = json!({
|
|
1225
|
+
"session_name": "team-rm039-status-working",
|
|
1226
|
+
"team_dir": "./.team/current",
|
|
1227
|
+
"active_team_key": active,
|
|
1228
|
+
// intentionally NO root "team_key" — the dirty shape.
|
|
1229
|
+
"agents": {
|
|
1230
|
+
"coder": {"status": "running", "first_send_at": "2026-01-01T00:00:00Z"}
|
|
1231
|
+
},
|
|
1232
|
+
"teams": {
|
|
1233
|
+
"current": {
|
|
1234
|
+
"active_team_key": active,
|
|
1235
|
+
"agents": {"coder": {"status": "running"}}
|
|
1236
|
+
},
|
|
1237
|
+
active: {
|
|
1238
|
+
"active_team_key": active,
|
|
1239
|
+
"agents": {"coder": {"status": "running"}}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
let runtime_dir = ws.join(".team").join("runtime");
|
|
1244
|
+
std::fs::create_dir_all(&runtime_dir).unwrap();
|
|
1245
|
+
std::fs::write(
|
|
1246
|
+
runtime_dir.join("state.json"),
|
|
1247
|
+
serde_json::to_vec_pretty(&raw).unwrap(),
|
|
1248
|
+
)
|
|
1249
|
+
.unwrap();
|
|
1250
|
+
// temp_ws() returns a unique path per call, so the cache
|
|
1251
|
+
// (keyed by path) does not collide with previous tests.
|
|
1252
|
+
|
|
1253
|
+
let loaded = load_runtime_state(&ws).expect("loaded state");
|
|
1254
|
+
assert_eq!(
|
|
1255
|
+
loaded.get("team_key").and_then(Value::as_str),
|
|
1256
|
+
Some(active),
|
|
1257
|
+
"load_runtime_state must promote team_key=active_team_key on the dirty shape"
|
|
1258
|
+
);
|
|
1259
|
+
// team_state_key now cascades to state.team_key, returning the
|
|
1260
|
+
// canonical key instead of the team_dir basename.
|
|
1261
|
+
assert_eq!(
|
|
1262
|
+
crate::state::projection::team_state_key(&loaded),
|
|
1263
|
+
active,
|
|
1264
|
+
"team_state_key must return the canonical key after migration"
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
// Re-read from disk (not cache) to confirm the migration was persisted.
|
|
1268
|
+
let on_disk: Value = serde_json::from_str(
|
|
1269
|
+
&std::fs::read_to_string(runtime_dir.join("state.json")).unwrap(),
|
|
1270
|
+
)
|
|
1271
|
+
.unwrap();
|
|
1272
|
+
assert_eq!(
|
|
1273
|
+
on_disk.get("team_key").and_then(Value::as_str),
|
|
1274
|
+
Some(active),
|
|
1275
|
+
"migration must be persisted to disk so subsequent processes see it"
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1040
1278
|
}
|
|
@@ -502,6 +502,31 @@ pub(crate) fn attach_command_for_workspace(
|
|
|
502
502
|
))
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
+
/// Bug #7 (prerelease 0.4.0 gate review §6): when the runtime state carries a
|
|
506
|
+
/// persisted `tmux_endpoint` / `tmux_socket` (e.g. `/private/tmp/tmux-501/default`),
|
|
507
|
+
/// the attach command MUST point at THAT endpoint, not the workspace-hash
|
|
508
|
+
/// socket — otherwise operators are told to attach to a socket where the
|
|
509
|
+
/// session does not exist. Falls back to workspace-hash when state has no
|
|
510
|
+
/// persisted endpoint.
|
|
511
|
+
pub(crate) fn attach_command_for_runtime_state_or_workspace(
|
|
512
|
+
workspace: &Path,
|
|
513
|
+
state: Option<&serde_json::Value>,
|
|
514
|
+
session_name: &SessionName,
|
|
515
|
+
window_name: &str,
|
|
516
|
+
) -> Option<String> {
|
|
517
|
+
if let Some((endpoint, _source)) = runtime_tmux_endpoint_from_state(state) {
|
|
518
|
+
let display = endpoint.to_string();
|
|
519
|
+
// Distinguish absolute path (`-S <path>`) from short socket name (`-L <name>`).
|
|
520
|
+
let flag = if Path::new(endpoint).is_absolute() { "-S" } else { "-L" };
|
|
521
|
+
return Some(format!(
|
|
522
|
+
"tmux {flag} {display} attach -t {}:{}",
|
|
523
|
+
session_name.as_str(),
|
|
524
|
+
window_name
|
|
525
|
+
));
|
|
526
|
+
}
|
|
527
|
+
attach_command_for_workspace(workspace, session_name, window_name)
|
|
528
|
+
}
|
|
529
|
+
|
|
505
530
|
pub(crate) fn attach_commands_for_windows<'a>(
|
|
506
531
|
workspace: &Path,
|
|
507
532
|
session_name: &SessionName,
|
package/npm/install.mjs
CHANGED
|
@@ -139,7 +139,11 @@ function install(argv) {
|
|
|
139
139
|
fs.mkdirSync(runtimeRoot, { recursive: true });
|
|
140
140
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
141
141
|
fs.mkdirSync(path.join(tmp, "bin"), { recursive: true });
|
|
142
|
-
|
|
142
|
+
const tmpBinary = path.join(tmp, "bin", "team-agent");
|
|
143
|
+
copyExecutable(platformBinary.path, tmpBinary);
|
|
144
|
+
// Sign the staged binary before swap. Failure aborts install before any
|
|
145
|
+
// rename touches the existing runtime directory.
|
|
146
|
+
prepareDarwinExecutable(tmpBinary);
|
|
143
147
|
|
|
144
148
|
fs.rmSync(backup, { recursive: true, force: true });
|
|
145
149
|
if (fs.existsSync(dest)) {
|
|
@@ -668,6 +672,28 @@ function copyExecutable(src, dest) {
|
|
|
668
672
|
fs.chmodSync(dest, 0o755);
|
|
669
673
|
}
|
|
670
674
|
|
|
675
|
+
// Defence-in-depth: on Darwin, ad-hoc re-sign the staged binary before the
|
|
676
|
+
// atomic rename swaps it into place. Atomic temp+rename already avoids the
|
|
677
|
+
// classic in-place vnode/signature-cache failure, but it does not repair an
|
|
678
|
+
// invalid or stale Mach-O signature that survived the npm extraction. The
|
|
679
|
+
// ad-hoc signature (-) is sufficient for local execution and avoids requiring
|
|
680
|
+
// a Developer ID. Fail closed before runtime swap if codesign exits non-zero.
|
|
681
|
+
function prepareDarwinExecutable(dest) {
|
|
682
|
+
if (process.platform !== "darwin") {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const res = spawnSync("/usr/bin/codesign", ["--force", "--sign", "-", dest], {
|
|
686
|
+
encoding: "utf8",
|
|
687
|
+
});
|
|
688
|
+
if (res.status !== 0) {
|
|
689
|
+
const stderr = (res.stderr || "").trim();
|
|
690
|
+
const signal = res.signal ? ` signal=${res.signal}` : "";
|
|
691
|
+
throw new Error(
|
|
692
|
+
`codesign failed on ${dest}: status=${res.status}${signal} stderr=${stderr || "(empty)"}`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
671
697
|
function writeExecWrapper(file, binary, fixedArgs, options = {}) {
|
|
672
698
|
if (fs.existsSync(file) && !options.allowForeign && !isInstallerManagedWrapper(file)) {
|
|
673
699
|
throw new Error(`refusing to overwrite non-Team Agent installer wrapper: ${file}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.4.0",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.4.0",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|