@team-agent/installer 0.5.31 → 0.5.32
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/src/cli/adapters.rs +0 -313
- package/crates/team-agent/src/cli/diagnose.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -157
- package/crates/team-agent/src/cli/send.rs +0 -241
- package/crates/team-agent/src/cli/spec.rs +0 -16
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -60
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +4 -54
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/types.rs +1 -64
- package/crates/team-agent/src/coordinator/health.rs +1 -1
- package/crates/team-agent/src/coordinator/tick.rs +36 -1
- package/crates/team-agent/src/db/agent_health_capture.rs +24 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +14 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +27 -2
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/packaging/mod.rs +1 -1
- package/crates/team-agent/src/packaging/types.rs +1 -1
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +5 -20
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +0 -374
- package/crates/team-agent/src/cli/tests/verb_settle.rs +0 -238
|
@@ -94,3 +94,27 @@ fn delete_agent_health(workspace: &Path, agent_id: &AgentId) -> Result<(), crate
|
|
|
94
94
|
)?;
|
|
95
95
|
Ok(())
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
99
|
+
/// clear the `(owner_team_id, agent_id)` health observation row on a new
|
|
100
|
+
/// worker process cohort boundary. Distinct from `delete_agent_health` (used
|
|
101
|
+
/// by remove-agent rollback): this narrow helper keyed on both owner_team_id
|
|
102
|
+
/// and agent_id so a sibling team with the same agent_id keeps its own row.
|
|
103
|
+
/// Silently no-ops when the row is absent.
|
|
104
|
+
pub fn clear_agent_health_observation(
|
|
105
|
+
workspace: &Path,
|
|
106
|
+
owner_team_id: &str,
|
|
107
|
+
agent_id: &AgentId,
|
|
108
|
+
) -> Result<(), crate::db::DbError> {
|
|
109
|
+
if owner_team_id.is_empty() {
|
|
110
|
+
return Ok(());
|
|
111
|
+
}
|
|
112
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
113
|
+
.map_err(|e| crate::db::DbError::Schema(e.to_string()))?;
|
|
114
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
115
|
+
conn.execute(
|
|
116
|
+
"delete from agent_health where owner_team_id = ?1 and agent_id = ?2",
|
|
117
|
+
rusqlite::params![owner_team_id, agent_id.as_str()],
|
|
118
|
+
)?;
|
|
119
|
+
Ok(())
|
|
120
|
+
}
|
|
@@ -284,6 +284,14 @@ pub(crate) fn start_agent_at_paths(
|
|
|
284
284
|
// after spawn" race with coordinator and no double source of truth.
|
|
285
285
|
crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
|
|
286
286
|
let team_key = restart_projection_team_key(&state, team);
|
|
287
|
+
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
288
|
+
// clear the matching `agent_health` observation on the new spawn cohort
|
|
289
|
+
// so five-line status summary and status --json do not surface a stale
|
|
290
|
+
// WORKING row from the pre-shutdown process. Best-effort: DB failure
|
|
291
|
+
// must not fail the start-agent path.
|
|
292
|
+
let _ = crate::db::agent_health_capture::clear_agent_health_observation(
|
|
293
|
+
workspace, &team_key, agent_id,
|
|
294
|
+
);
|
|
287
295
|
let skip_capture_backfill = if matches!(
|
|
288
296
|
start_mode,
|
|
289
297
|
StartMode::Fresh | StartMode::FreshAfterMissingRollout
|
|
@@ -977,6 +985,12 @@ fn mark_agent_started(
|
|
|
977
985
|
agent_id
|
|
978
986
|
)));
|
|
979
987
|
};
|
|
988
|
+
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
989
|
+
// a successful new process cohort invalidates the per-agent
|
|
990
|
+
// turn/activity observation set. Do this before overwriting the
|
|
991
|
+
// lifecycle/topology fields so absence == UNKNOWN until the next
|
|
992
|
+
// coordinator tick or pane fallback produces a post-spawn observation.
|
|
993
|
+
clear_agent_runtime_activity_observation(agent);
|
|
980
994
|
// S1-CAPTURE-001 (0.4.8, CR M3 provider-agnostic): on a Fresh /
|
|
981
995
|
// FreshAfterMissingRollout start, the prior session's authoritative
|
|
982
996
|
// capture tuple MUST be cleared before persist_command_plan_state
|
|
@@ -1601,6 +1601,33 @@ pub(super) fn state_spawn_epoch_for_agent(workspace: &Path, agent_id: &AgentId)
|
|
|
1601
1601
|
.unwrap_or(0)
|
|
1602
1602
|
}
|
|
1603
1603
|
|
|
1604
|
+
/// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
1605
|
+
/// clear the per-agent turn/activity observation set on a successful new
|
|
1606
|
+
/// worker process cohort. Called from `mark_agent_started` /
|
|
1607
|
+
/// `mark_agent_respawned` / `mark_fake_harness_agent_respawned`; NOT from
|
|
1608
|
+
/// `mark_agent_running_noop` (no new process was created there).
|
|
1609
|
+
///
|
|
1610
|
+
/// Removes only observation fields — never lifecycle/topology fields. After
|
|
1611
|
+
/// clearing, absence is UNKNOWN, not synthesized idle (T7 unknown-never-idle
|
|
1612
|
+
/// discipline); the next post-spawn tick may repopulate from JSONL freshness
|
|
1613
|
+
/// gate + pane fallback.
|
|
1614
|
+
pub(super) fn clear_agent_runtime_activity_observation(
|
|
1615
|
+
agent: &mut serde_json::Map<String, serde_json::Value>,
|
|
1616
|
+
) {
|
|
1617
|
+
for field in [
|
|
1618
|
+
"activity",
|
|
1619
|
+
"worker_state",
|
|
1620
|
+
"last_output_at",
|
|
1621
|
+
"last_output_hash",
|
|
1622
|
+
"current_turn_message_id",
|
|
1623
|
+
"current_task_id", // ALLOWED-LEGACY-READ (Phase-DX E2): cleanup removal of legacy display-only key on new spawn cohort; helper does not read the value.
|
|
1624
|
+
"task_id",
|
|
1625
|
+
"coordinator_idle_capture_next_at",
|
|
1626
|
+
] {
|
|
1627
|
+
agent.remove(field);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1604
1631
|
#[cfg(test)]
|
|
1605
1632
|
mod e36_transcript_backing_tests {
|
|
1606
1633
|
use super::*;
|
|
@@ -433,8 +433,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
433
433
|
&decision.agent_id,
|
|
434
434
|
&raw_agent,
|
|
435
435
|
);
|
|
436
|
-
if endpoint_convergence_fake_harness_enabled(&state)
|
|
437
|
-
&& is_fake_model_harness_agent(&agent)
|
|
436
|
+
if endpoint_convergence_fake_harness_enabled(&state) && is_fake_model_harness_agent(&agent)
|
|
438
437
|
{
|
|
439
438
|
write_fake_harness_spawn_argv_event(
|
|
440
439
|
&selected.run_workspace,
|
|
@@ -449,6 +448,13 @@ fn restart_with_selected_team_and_transport(
|
|
|
449
448
|
&session_name,
|
|
450
449
|
&selected.team_key,
|
|
451
450
|
);
|
|
451
|
+
// 0.5.32: fake harness respawn shares the same spawn cohort
|
|
452
|
+
// boundary — clear the matching `agent_health` observation.
|
|
453
|
+
let _ = crate::db::agent_health_capture::clear_agent_health_observation(
|
|
454
|
+
&selected.run_workspace,
|
|
455
|
+
&selected.team_key,
|
|
456
|
+
&decision.agent_id,
|
|
457
|
+
);
|
|
452
458
|
successful_agents.push(decision.clone());
|
|
453
459
|
continue;
|
|
454
460
|
}
|
|
@@ -556,6 +562,15 @@ fn restart_with_selected_team_and_transport(
|
|
|
556
562
|
{
|
|
557
563
|
persist_effective_approval_policy_for_restart(agent, &safety);
|
|
558
564
|
}
|
|
565
|
+
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
566
|
+
// pair the state-side activity clear with a DB `agent_health` clear so
|
|
567
|
+
// status --json's health projection does not surface the pre-restart
|
|
568
|
+
// WORKING row. Best-effort: DB failure must not fail the restart loop.
|
|
569
|
+
let _ = crate::db::agent_health_capture::clear_agent_health_observation(
|
|
570
|
+
&selected.run_workspace,
|
|
571
|
+
&selected.team_key,
|
|
572
|
+
&decision.agent_id,
|
|
573
|
+
);
|
|
559
574
|
}
|
|
560
575
|
// END_B5_RESTART_ISOLATION_LOOP
|
|
561
576
|
let mut topology_authority_agent_ids = successful_agents
|
|
@@ -1630,6 +1645,12 @@ fn mark_agent_respawned(
|
|
|
1630
1645
|
agent_id
|
|
1631
1646
|
)));
|
|
1632
1647
|
};
|
|
1648
|
+
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
1649
|
+
// multi-worker restart respawn is a new process cohort; clear the
|
|
1650
|
+
// per-agent turn/activity observation set before overwriting lifecycle
|
|
1651
|
+
// fields so stale `activity=working` / `worker_state=BUSY` do not
|
|
1652
|
+
// survive into the fresh cohort.
|
|
1653
|
+
clear_agent_runtime_activity_observation(agent);
|
|
1633
1654
|
agent.insert("status".to_string(), serde_json::json!("running"));
|
|
1634
1655
|
agent.insert(
|
|
1635
1656
|
"window".to_string(),
|
|
@@ -1785,6 +1806,10 @@ fn mark_fake_harness_agent_respawned(
|
|
|
1785
1806
|
else {
|
|
1786
1807
|
return;
|
|
1787
1808
|
};
|
|
1809
|
+
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
1810
|
+
// fake harness respawn is also a new process cohort — RED fixtures must
|
|
1811
|
+
// not preserve stale activity observations across restart.
|
|
1812
|
+
clear_agent_runtime_activity_observation(agent);
|
|
1788
1813
|
agent.insert("status".to_string(), serde_json::json!("running"));
|
|
1789
1814
|
agent.insert("window".to_string(), serde_json::json!(agent_id.as_str()));
|
|
1790
1815
|
agent.insert(
|
|
@@ -57,7 +57,7 @@ fn stuck_cancel_snapshot_delivered_message_ids_uses_golden_status_set() {
|
|
|
57
57
|
// is `store.add_result(envelope)`'d (results.py:73) regardless of any in-flight delivery, then the
|
|
58
58
|
// collection loop collects it when its task_id is a known task (state.tasks) OR message-scoped (msg_+
|
|
59
59
|
// matching message). NO live in-flight task is required at ingest time. rt-host-b @ c262bf7 saw a
|
|
60
|
-
// VALID envelope
|
|
60
|
+
// VALID envelope collect to exit-1 / empty output / NOT in the
|
|
61
61
|
// results table — i.e. the --result-file ingest path was a no-op (results.rs once did
|
|
62
62
|
// `let _ = (result_file, …)`). This pins the happy path: a valid envelope for a KNOWN task must be
|
|
63
63
|
// ingested into the results table AND collected, with ok=true. (Completes the previously-deferred
|
|
@@ -75,7 +75,7 @@ use crate::model::enums::Provider;
|
|
|
75
75
|
// ── REUSE — step 4 event_log(install/upgrade/repair 审计事件;原子替换的 rebuild/rollback 标记)──
|
|
76
76
|
use crate::event_log::EventLog;
|
|
77
77
|
|
|
78
|
-
// ── REUSE — step 5 state(uninstall「有 team 在跑勿删」判定经 state
|
|
78
|
+
// ── REUSE — step 5 state(uninstall「有 team 在跑勿删」判定经 state 投影)──
|
|
79
79
|
// `state` 操作 state.json = serde_json::Value;此处仅在 fn 签名内经全限定路径引用,避免未用顶层 import。
|
|
80
80
|
|
|
81
81
|
#[cfg(test)]
|
|
@@ -400,7 +400,7 @@ pub enum PackagingError {
|
|
|
400
400
|
/// step 3 schema/migration 转调错误。
|
|
401
401
|
#[error("schema: {0}")]
|
|
402
402
|
Db(#[from] DbError),
|
|
403
|
-
/// step 5 state(
|
|
403
|
+
/// step 5 state(team-running 判定)转调错误。
|
|
404
404
|
#[error("state: {0}")]
|
|
405
405
|
State(String),
|
|
406
406
|
/// model 校验/解析转调(版本解析等)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
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.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.32",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.32",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.32"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -44,7 +44,7 @@ Fresh/reset red line:
|
|
|
44
44
|
| Leader pane exists but messages say `leader_not_attached`, `rebind_required`, `team_owner_mismatch`, or leader 被抢 | Leader ownership or receiver binding drift | §2 L3 Claim Or Rebind Leader |
|
|
45
45
|
| A live team must be recovered without losing worker context | Resume/restart path needed | §3 L2 Restart With Resume First |
|
|
46
46
|
| Shutdown/restart cleanup status is ambiguous | Need residue-based truth, not a single `ok` field | §4 L1/L2 Cleanup Verification |
|
|
47
|
-
| MCP tool call fails with `Transport closed` while the worker still has the payload | Provider-owned MCP stdio child died | §5
|
|
47
|
+
| MCP tool call fails with `Transport closed` while the worker still has the payload | Provider-owned MCP stdio child died | §5 MCP Transport-Dead Payload Handoff |
|
|
48
48
|
|
|
49
49
|
## 1. L1 Read-Only Triage
|
|
50
50
|
|
|
@@ -239,30 +239,15 @@ Python 0.2.11 compatibility note:
|
|
|
239
239
|
|
|
240
240
|
- Python cleanup notes may mention direct tmux commands. They are not listed here as RS recovery prescriptions.
|
|
241
241
|
|
|
242
|
-
## 5.
|
|
242
|
+
## 5. MCP Transport-Dead Payload Handoff
|
|
243
243
|
|
|
244
|
-
[contextual: preserved] [level: L1]
|
|
244
|
+
[contextual: preserved] [level: L1] If the worker already has a leader-bound payload in hand and the MCP transport dies before the normal `send` or `report_result` path can accept it, preserve the payload in a file handoff artifact and ask for leader/user recovery through the visible channel that still works.
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- Product implementation exists in the E23 branch for `fallback-send-leader` and `fallback-report-result`.
|
|
249
|
-
- Local RS verification has covered CLI help and focused E23 tests.
|
|
250
|
-
- This reference does not yet promote the fallback CLI to a general external prescription because the new documentation gate requires current-release RS validation in the Mac mini environment.
|
|
251
|
-
|
|
252
|
-
Until that validation is added:
|
|
253
|
-
|
|
254
|
-
- Workers should write a file handoff artifact for the current payload and ask for leader/user recovery.
|
|
255
|
-
- Leaders should prefer restart/resume of the affected worker after the current payload is preserved.
|
|
256
|
-
|
|
257
|
-
Do not use E23 fallback as a general message path. It does not permit arbitrary worker execution of L2/L3 recovery operations.
|
|
258
|
-
|
|
259
|
-
RS verification record:
|
|
260
|
-
|
|
261
|
-
- Local only at the time of this reference update: E23 focused tests and CLI help on the merged E23 documentation branch. Mac mini current-release validation is still required before adding exact fallback commands here.
|
|
246
|
+
After restart/resume, use the normal MCP tool path again. Do not treat file handoff as a general message path or as permission to run arbitrary L2/L3 recovery operations.
|
|
262
247
|
|
|
263
248
|
Python 0.2.11 compatibility note:
|
|
264
249
|
|
|
265
|
-
- Python MCP stdio EOF had no durable reconnect. File handoff was the practical fallback.
|
|
250
|
+
- Python MCP stdio EOF had no durable reconnect. File handoff was the practical fallback.
|
|
266
251
|
|
|
267
252
|
## Held Out Of This Reference
|
|
268
253
|
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
use super::*;
|
|
2
|
-
|
|
3
|
-
const REPAIR_SPEC_TEMPLATE: &str = r#"version: 1
|
|
4
|
-
team:
|
|
5
|
-
name: "repair-team"
|
|
6
|
-
mode: "supervisor_worker"
|
|
7
|
-
objective: "Exercise repair-state."
|
|
8
|
-
workspace: "__WS__"
|
|
9
|
-
leader:
|
|
10
|
-
id: "leader"
|
|
11
|
-
role: "leader"
|
|
12
|
-
provider: "fake"
|
|
13
|
-
model: null
|
|
14
|
-
tools:
|
|
15
|
-
- "fs_read"
|
|
16
|
-
- "fs_list"
|
|
17
|
-
- "mcp_team"
|
|
18
|
-
context_policy:
|
|
19
|
-
keep_user_thread: true
|
|
20
|
-
receive_worker_outputs: "structured_only"
|
|
21
|
-
max_worker_result_tokens: 2000
|
|
22
|
-
agents:
|
|
23
|
-
- id: "fake_impl"
|
|
24
|
-
role: "implementation_engineer"
|
|
25
|
-
provider: "fake"
|
|
26
|
-
model: null
|
|
27
|
-
working_directory: "__WS__"
|
|
28
|
-
system_prompt:
|
|
29
|
-
inline: "Handle fake implementation tasks."
|
|
30
|
-
file: null
|
|
31
|
-
tools:
|
|
32
|
-
- "fs_read"
|
|
33
|
-
- "mcp_team"
|
|
34
|
-
permission_mode: "restricted"
|
|
35
|
-
preferred_for:
|
|
36
|
-
- "implementation"
|
|
37
|
-
avoid_for: []
|
|
38
|
-
output_contract:
|
|
39
|
-
format: "result_envelope_v1"
|
|
40
|
-
required_fields:
|
|
41
|
-
- "task_id"
|
|
42
|
-
- "status"
|
|
43
|
-
- "summary"
|
|
44
|
-
- "artifacts"
|
|
45
|
-
routing:
|
|
46
|
-
default_assignee: "leader"
|
|
47
|
-
rules:
|
|
48
|
-
- id: "implementation-to-fake"
|
|
49
|
-
match:
|
|
50
|
-
type:
|
|
51
|
-
- "implementation"
|
|
52
|
-
assign_to: "fake_impl"
|
|
53
|
-
priority: 10
|
|
54
|
-
communication:
|
|
55
|
-
protocol: "mcp_inbox"
|
|
56
|
-
topology: "leader_centered"
|
|
57
|
-
worker_to_worker: true
|
|
58
|
-
ack_timeout_sec: 2
|
|
59
|
-
result_format: "result_envelope_v1"
|
|
60
|
-
message_store:
|
|
61
|
-
sqlite: ".team/runtime/team.db"
|
|
62
|
-
mirror_files: ".team/messages"
|
|
63
|
-
runtime:
|
|
64
|
-
backend: "tmux"
|
|
65
|
-
display_backend: "none"
|
|
66
|
-
session_name: "team-agent-repair"
|
|
67
|
-
auto_launch: true
|
|
68
|
-
require_user_approval_before_launch: false
|
|
69
|
-
max_active_agents: 1
|
|
70
|
-
startup_order:
|
|
71
|
-
- "fake_impl"
|
|
72
|
-
context:
|
|
73
|
-
state_file: "team_state.md"
|
|
74
|
-
artifact_dir: ".team/artifacts"
|
|
75
|
-
log_dir: ".team/logs"
|
|
76
|
-
summarization:
|
|
77
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
78
|
-
state_update: "after_each_result"
|
|
79
|
-
tasks:
|
|
80
|
-
- id: "task_impl"
|
|
81
|
-
title: "Fake implementation"
|
|
82
|
-
type: "implementation"
|
|
83
|
-
assignee: null
|
|
84
|
-
deps: []
|
|
85
|
-
acceptance:
|
|
86
|
-
- "fake result collected"
|
|
87
|
-
status: "pending"
|
|
88
|
-
"#;
|
|
89
|
-
|
|
90
|
-
fn seed_repair_workspace(ws: &std::path::Path) {
|
|
91
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
92
|
-
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
93
|
-
let spec_path = ws.join("team.spec.yaml");
|
|
94
|
-
std::fs::write(
|
|
95
|
-
&spec_path,
|
|
96
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
97
|
-
)
|
|
98
|
-
.unwrap();
|
|
99
|
-
crate::state::persist::save_runtime_state(
|
|
100
|
-
ws,
|
|
101
|
-
&json!({
|
|
102
|
-
"spec_path": spec_path.to_string_lossy(),
|
|
103
|
-
"session_name": "team-agent-repair",
|
|
104
|
-
"leader": {"id": "leader"},
|
|
105
|
-
"agents": {"fake_impl": {"status": "stopped", "provider": "fake"}},
|
|
106
|
-
"tasks": [{
|
|
107
|
-
"id": "task_impl",
|
|
108
|
-
"title": "Fake implementation",
|
|
109
|
-
"type": "implementation",
|
|
110
|
-
"assignee": null,
|
|
111
|
-
"deps": [],
|
|
112
|
-
"acceptance": ["fake result collected"],
|
|
113
|
-
"status": "pending"
|
|
114
|
-
}]
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
.unwrap();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
fn repair_args(
|
|
121
|
-
ws: &std::path::Path,
|
|
122
|
-
status: &str,
|
|
123
|
-
summary: Option<&str>,
|
|
124
|
-
json: bool,
|
|
125
|
-
) -> RepairStateArgs {
|
|
126
|
-
RepairStateArgs {
|
|
127
|
-
workspace: ws.to_path_buf(),
|
|
128
|
-
task_id: "task_impl".to_string(),
|
|
129
|
-
assignee: Some("fake_impl".to_string()),
|
|
130
|
-
status: status.to_string(),
|
|
131
|
-
summary: summary.map(str::to_string),
|
|
132
|
-
json,
|
|
133
|
-
team: None,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fn team_repair_state(ws: &std::path::Path, team: &str) -> serde_json::Value {
|
|
138
|
-
json!({
|
|
139
|
-
"active_team_key": team,
|
|
140
|
-
"status": "alive",
|
|
141
|
-
"spec_path": ws.join(".team").join("runtime").join(team).join("team.spec.yaml").to_string_lossy(),
|
|
142
|
-
"session_name": format!("team-agent-{team}"),
|
|
143
|
-
"leader": {"id": "leader"},
|
|
144
|
-
"agents": {"fake_impl": {"status": "stopped", "provider": "fake"}},
|
|
145
|
-
"tasks": [{
|
|
146
|
-
"id": "task_impl",
|
|
147
|
-
"title": format!("{team} implementation"),
|
|
148
|
-
"type": "implementation",
|
|
149
|
-
"assignee": null,
|
|
150
|
-
"deps": [],
|
|
151
|
-
"acceptance": ["fake result collected"],
|
|
152
|
-
"status": "pending"
|
|
153
|
-
}]
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fn seed_two_team_repair_workspace(ws: &std::path::Path) {
|
|
158
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
159
|
-
for team in ["alpha", "beta"] {
|
|
160
|
-
let spec_dir = ws.join(".team").join("runtime").join(team);
|
|
161
|
-
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
162
|
-
std::fs::write(
|
|
163
|
-
spec_dir.join("team.spec.yaml"),
|
|
164
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
165
|
-
)
|
|
166
|
-
.unwrap();
|
|
167
|
-
}
|
|
168
|
-
crate::state::persist::save_runtime_state(
|
|
169
|
-
ws,
|
|
170
|
-
&json!({
|
|
171
|
-
"active_team_key": "alpha",
|
|
172
|
-
"teams": {
|
|
173
|
-
"alpha": team_repair_state(ws, "alpha"),
|
|
174
|
-
"beta": team_repair_state(ws, "beta"),
|
|
175
|
-
}
|
|
176
|
-
}),
|
|
177
|
-
)
|
|
178
|
-
.unwrap();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Golden source:
|
|
182
|
-
// - cli/parser.py:303-310 registers `repair-state --workspace --task --assignee --status --summary --json`.
|
|
183
|
-
// - cli/commands.py:196-203 delegates all five args to `runtime.repair_state`.
|
|
184
|
-
// - diagnose/quick_start.py:285-324:
|
|
185
|
-
// * loads state + spec, validates assignee against spec agents plus leader id,
|
|
186
|
-
// * validates status against task_graph.py TASK_STATUSES,
|
|
187
|
-
// * before/after are three-field projections `{assignee,status,last_result_summary}`,
|
|
188
|
-
// * summary writes `last_result_summary` (not `summary`),
|
|
189
|
-
// * writes EventLog event `repair_state.task` with task_id/before/after,
|
|
190
|
-
// * returns `{ok,task_id,before,after,state_file}` in that insertion order.
|
|
191
|
-
// - task_graph.py:5-14 legal statuses are exactly:
|
|
192
|
-
// blocked,cancelled,done,failed,needs_retry,pending,ready,running.
|
|
193
|
-
//
|
|
194
|
-
// Golden probe:
|
|
195
|
-
// PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src python3 /tmp/probe_repair_state_cli.py
|
|
196
|
-
#[test]
|
|
197
|
-
fn repair_state_success_json_human_and_event_byte_shape() {
|
|
198
|
-
let ws = tmp_workspace();
|
|
199
|
-
seed_repair_workspace(&ws);
|
|
200
|
-
|
|
201
|
-
let json_result = cmd_repair_state(&repair_args(&ws, "done", Some("patched"), true))
|
|
202
|
-
.expect("repair-state success should return ok");
|
|
203
|
-
assert_eq!(json_result.exit, ExitCode::Ok);
|
|
204
|
-
assert_eq!(
|
|
205
|
-
emit(&json_result.output, true).unwrap(),
|
|
206
|
-
format!(
|
|
207
|
-
"{{\n \"after\": {{\n \"assignee\": \"fake_impl\",\n \"last_result_summary\": \"patched\",\n \"status\": \"done\"\n }},\n \"before\": {{\n \"assignee\": null,\n \"last_result_summary\": null,\n \"status\": \"pending\"\n }},\n \"ok\": true,\n \"state_file\": \"{}\",\n \"task_id\": \"task_impl\"\n}}",
|
|
208
|
-
ws.join("team_state.md").to_string_lossy()
|
|
209
|
-
),
|
|
210
|
-
"repair-state --json must match Python pretty sorted JSON byte shape",
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
let event_line = std::fs::read_to_string(ws.join(".team/logs/events.jsonl"))
|
|
214
|
-
.expect("repair-state must write events.jsonl")
|
|
215
|
-
.lines()
|
|
216
|
-
.last()
|
|
217
|
-
.expect("repair-state must append an event")
|
|
218
|
-
.to_string();
|
|
219
|
-
assert!(
|
|
220
|
-
event_line.starts_with("{\"after\": {\"assignee\": \"fake_impl\", \"last_result_summary\": \"patched\", \"status\": \"done\"}, \"before\": {\"assignee\": null, \"last_result_summary\": null, \"status\": \"pending\"}, \"event\": \"repair_state.task\", \"task_id\": \"task_impl\", \"ts\": \""),
|
|
221
|
-
"repair_state.task event must be Python sort_keys JSON with after/before/event/task_id/ts order; got {event_line}",
|
|
222
|
-
);
|
|
223
|
-
assert!(event_line.ends_with("\"}"), "event line must end with timestamp string; got {event_line}");
|
|
224
|
-
|
|
225
|
-
let ws_human = tmp_workspace();
|
|
226
|
-
seed_repair_workspace(&ws_human);
|
|
227
|
-
let human_result = cmd_repair_state(&repair_args(&ws_human, "done", Some("patched"), false))
|
|
228
|
-
.expect("repair-state human success should return ok");
|
|
229
|
-
assert_eq!(
|
|
230
|
-
emit(&human_result.output, false).unwrap(),
|
|
231
|
-
format!(
|
|
232
|
-
"ok: True\ntask_id: task_impl\nbefore: {{\"assignee\": null, \"status\": \"pending\", \"last_result_summary\": null}}\nafter: {{\"assignee\": \"fake_impl\", \"status\": \"done\", \"last_result_summary\": \"patched\"}}\nstate_file: {}",
|
|
233
|
-
ws_human.join("team_state.md").to_string_lossy()
|
|
234
|
-
),
|
|
235
|
-
"repair-state human output must preserve Python returned-dict and nested-dict insertion order",
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
239
|
-
assert_eq!(
|
|
240
|
-
state["tasks"][0]["last_result_summary"],
|
|
241
|
-
json!("patched"),
|
|
242
|
-
"golden writes summary into last_result_summary, not summary",
|
|
243
|
-
);
|
|
244
|
-
assert!(
|
|
245
|
-
state["tasks"][0].get("summary").is_none(),
|
|
246
|
-
"golden does not create a task.summary field during repair-state",
|
|
247
|
-
);
|
|
248
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
249
|
-
let _ = std::fs::remove_dir_all(&ws_human);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#[test]
|
|
253
|
-
fn repair_state_rejects_status_outside_task_statuses() {
|
|
254
|
-
let ws = tmp_workspace();
|
|
255
|
-
seed_repair_workspace(&ws);
|
|
256
|
-
let legal_statuses = vec![
|
|
257
|
-
"blocked",
|
|
258
|
-
"cancelled",
|
|
259
|
-
"done",
|
|
260
|
-
"failed",
|
|
261
|
-
"needs_retry",
|
|
262
|
-
"pending",
|
|
263
|
-
"ready",
|
|
264
|
-
"running",
|
|
265
|
-
];
|
|
266
|
-
assert_eq!(
|
|
267
|
-
legal_statuses,
|
|
268
|
-
vec![
|
|
269
|
-
"blocked",
|
|
270
|
-
"cancelled",
|
|
271
|
-
"done",
|
|
272
|
-
"failed",
|
|
273
|
-
"needs_retry",
|
|
274
|
-
"pending",
|
|
275
|
-
"ready",
|
|
276
|
-
"running"
|
|
277
|
-
],
|
|
278
|
-
"golden TASK_STATUSES from task_graph.py:5 must stay locked",
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
let err = cmd_repair_state(&repair_args(&ws, "assigned", None, true))
|
|
282
|
-
.expect_err("status outside TASK_STATUSES must error");
|
|
283
|
-
assert_eq!(err.to_string(), "unknown task status for repair: assigned");
|
|
284
|
-
let payload = err.to_payload(&ws.join(".team/logs/cli-error-123.log"), "repair-state");
|
|
285
|
-
assert_eq!(
|
|
286
|
-
serde_json::to_string(&payload).unwrap(),
|
|
287
|
-
format!(
|
|
288
|
-
"{{\"ok\":false,\"error\":\"unknown task status for repair: assigned\",\"action\":\"run `team-agent doctor` or inspect the log path shown here\",\"log\":\"{}\"}}",
|
|
289
|
-
ws.join(".team/logs/cli-error-123.log").to_string_lossy()
|
|
290
|
-
),
|
|
291
|
-
"repair-state --json error envelope must match Python compact key order and text",
|
|
292
|
-
);
|
|
293
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
#[test]
|
|
297
|
-
fn repair_state_explicit_team_updates_selected_team_not_active_team() {
|
|
298
|
-
let ws = tmp_workspace();
|
|
299
|
-
seed_two_team_repair_workspace(&ws);
|
|
300
|
-
let mut args = repair_args(&ws, "done", Some("patched beta"), true);
|
|
301
|
-
args.team = Some("beta".to_string());
|
|
302
|
-
|
|
303
|
-
let cmd = cmd_repair_state(&args).expect("repair-state --team beta should update beta");
|
|
304
|
-
assert_eq!(cmd.exit, ExitCode::Ok);
|
|
305
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
306
|
-
assert_eq!(
|
|
307
|
-
state["teams"]["alpha"]["tasks"][0]["status"],
|
|
308
|
-
json!("pending"),
|
|
309
|
-
"repair-state --team beta must not mutate active/default alpha"
|
|
310
|
-
);
|
|
311
|
-
assert_eq!(
|
|
312
|
-
state["teams"]["beta"]["tasks"][0]["status"],
|
|
313
|
-
json!("done"),
|
|
314
|
-
"repair-state must consume args.team and update beta"
|
|
315
|
-
);
|
|
316
|
-
assert_eq!(
|
|
317
|
-
state["teams"]["beta"]["tasks"][0]["last_result_summary"],
|
|
318
|
-
json!("patched beta")
|
|
319
|
-
);
|
|
320
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// CONTRACT (real-machine repair_state product FAIL): rt-host-d ran `repair-state --task rp1 --status done`
|
|
324
|
-
// in a quick-start workspace whose spec lives in a teamdir (state.spec_path -> <ws>/teamdir/team.spec.yaml),
|
|
325
|
-
// NOT at <ws>/team.spec.yaml. Rust cmd_repair_state calls load_team_spec(workspace) which HARDCODES
|
|
326
|
-
// workspace.join("team.spec.yaml") (adapters.rs:982) -> read_to_string fails 'No such file or directory'.
|
|
327
|
-
// golden (quick_start.py:295) resolves spec_path = state.get("spec_path", workspace/"team.spec.yaml") and
|
|
328
|
-
// SUCCEEDS. NB: golden ALSO writes team_state.md (write_team_state -> state.py mkdir+write) and returns
|
|
329
|
-
// state_file=team_state.md, so the fix is NOT to drop write_team_state (that diverges from golden and breaks
|
|
330
|
-
// repair_state_success_json_human_and_event_byte_shape) — it is to resolve the spec from state.spec_path
|
|
331
|
-
// (Rust already has load_team_spec_optional(workspace, state) that does exactly this).
|
|
332
|
-
#[test]
|
|
333
|
-
fn contract_repair_state_resolves_spec_from_state_spec_path_teamdir_layout() {
|
|
334
|
-
let ws = tmp_workspace();
|
|
335
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
336
|
-
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
337
|
-
let teamdir = ws.join("teamdir");
|
|
338
|
-
std::fs::create_dir_all(&teamdir).unwrap();
|
|
339
|
-
let spec_path = teamdir.join("team.spec.yaml");
|
|
340
|
-
std::fs::write(
|
|
341
|
-
&spec_path,
|
|
342
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
343
|
-
)
|
|
344
|
-
.unwrap();
|
|
345
|
-
// NOTE: NO <ws>/team.spec.yaml (the hardcoded path) and NO pre-existing team_state.md.
|
|
346
|
-
crate::state::persist::save_runtime_state(
|
|
347
|
-
&ws,
|
|
348
|
-
&json!({
|
|
349
|
-
"spec_path": spec_path.to_string_lossy(),
|
|
350
|
-
"session_name": "team-agent-repair",
|
|
351
|
-
"leader": {"id": "leader"},
|
|
352
|
-
"agents": {"fake_impl": {"status": "running", "provider": "fake"}},
|
|
353
|
-
"tasks": [{
|
|
354
|
-
"id": "task_impl", "title": "x", "type": "implementation",
|
|
355
|
-
"assignee": null, "deps": [], "acceptance": ["a"], "status": "pending"
|
|
356
|
-
}]
|
|
357
|
-
}),
|
|
358
|
-
)
|
|
359
|
-
.unwrap();
|
|
360
|
-
|
|
361
|
-
let cmd = cmd_repair_state(&repair_args(&ws, "done", Some("patched"), true)).expect(
|
|
362
|
-
"CONTRACT: repair-state --status done must resolve the spec from state.spec_path (golden \
|
|
363
|
-
quick_start.py:295), not the hardcoded <ws>/team.spec.yaml (adapters.rs:982) which is absent in \
|
|
364
|
-
the teamdir layout -> currently io 'No such file or directory'",
|
|
365
|
-
);
|
|
366
|
-
assert_eq!(cmd.exit, ExitCode::Ok);
|
|
367
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
368
|
-
assert_eq!(
|
|
369
|
-
state["tasks"][0]["status"],
|
|
370
|
-
json!("done"),
|
|
371
|
-
"the task must persist status=done after repair"
|
|
372
|
-
);
|
|
373
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
374
|
-
}
|