@team-agent/installer 0.2.5 → 0.2.7
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/README.md +22 -0
- package/npm/bincheck.mjs +70 -0
- package/package.json +2 -1
- package/skills/team-agent/references/bug-as-artifact-flow.md +82 -0
- package/src/team_agent/_legacy_pane_discovery.py +189 -0
- package/src/team_agent/cli/helpers.py +89 -0
- package/src/team_agent/cli/parser.py +5 -0
- package/src/team_agent/diagnose/quick_start.py +1 -1
- package/src/team_agent/leader_binding.py +183 -0
- package/src/team_agent/mcp_server/tools.py +211 -64
- package/src/team_agent/message_store/schema.py +2 -9
- package/src/team_agent/message_store/schema_migration.py +123 -63
- package/src/team_agent/messaging/deps.py +1 -17
- package/src/team_agent/messaging/leader.py +2 -3
- package/src/team_agent/messaging/leader_panes.py +43 -166
- package/src/team_agent/messaging/scheduler.py +1 -1
- package/src/team_agent/provider_cli/adapter.py +10 -5
- package/src/team_agent/provider_cli/codex.py +26 -9
- package/src/team_agent/restart/orchestration.py +12 -0
- package/src/team_agent/runtime.py +246 -79
- package/src/team_agent/state.py +146 -31
package/README.md
CHANGED
|
@@ -103,6 +103,28 @@ cd team-agent
|
|
|
103
103
|
npm exec --yes --package . -- team-agent-installer install
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### Windows + WSL
|
|
107
|
+
|
|
108
|
+
If you run `npx @team-agent/installer@latest install` from
|
|
109
|
+
`/mnt/c/Users/<user>/` and see npm warn
|
|
110
|
+
`config prefix cannot be changed from project config`, followed by
|
|
111
|
+
`team-agent-installer: not found`, npm did not expose the installer bin shim.
|
|
112
|
+
|
|
113
|
+
This is usually caused by a project-level `.npmrc` under `/mnt/c/Users/<user>/`
|
|
114
|
+
that contains a `prefix=...` setting. Action: move that setting to `~/.npmrc`,
|
|
115
|
+
or delete the project-level `prefix` line, then retry from your Linux home:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd ~
|
|
119
|
+
npx @team-agent/installer@latest install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Fallback explicit package form:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx --package @team-agent/installer team-agent-installer install
|
|
126
|
+
```
|
|
127
|
+
|
|
106
128
|
### Use
|
|
107
129
|
|
|
108
130
|
Start the lead inside tmux. The shortcut commands create or attach a tmux
|
package/npm/bincheck.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const BIN_NAME = "team-agent-installer";
|
|
6
|
+
const SELF_CHECK_ONLY = process.env.TEAM_AGENT_INSTALLER_SELF_CHECK_ONLY === "1";
|
|
7
|
+
|
|
8
|
+
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
9
|
+
const pathValue = process.env.PATH || "";
|
|
10
|
+
|
|
11
|
+
if (!findOnPath(BIN_NAME, pathValue)) {
|
|
12
|
+
printMissingBinDiagnostic(initCwd, pathValue);
|
|
13
|
+
process.exit(SELF_CHECK_ONLY ? 1 : 0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (SELF_CHECK_ONLY) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findOnPath(commandName, searchPath) {
|
|
21
|
+
const extensions = process.platform === "win32" ? ["", ".cmd", ".bat", ".ps1", ".exe"] : [""];
|
|
22
|
+
for (const directory of searchPath.split(path.delimiter)) {
|
|
23
|
+
if (!directory) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
for (const extension of extensions) {
|
|
27
|
+
const candidate = path.join(directory, `${commandName}${extension}`);
|
|
28
|
+
try {
|
|
29
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
30
|
+
return candidate;
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printMissingBinDiagnostic(cwd, searchPath) {
|
|
40
|
+
const npmrcPath = path.join(cwd, ".npmrc");
|
|
41
|
+
const npmrcSummary = summarizeNpmrc(npmrcPath);
|
|
42
|
+
const wslHint = isWslMntC(cwd) ? "yes" : "unknown";
|
|
43
|
+
const pathEntries = searchPath ? searchPath.split(path.delimiter).length : 0;
|
|
44
|
+
|
|
45
|
+
console.error("ERROR: team-agent-installer bin not on PATH after npm install.");
|
|
46
|
+
console.error("ACTION: This is common when WSL runs npx from /mnt/c and a project-level .npmrc sets prefix.");
|
|
47
|
+
console.error(" - Move that setting to ~/.npmrc, or delete the project-level prefix line.");
|
|
48
|
+
console.error(" - Then cd ~ and rerun `npx @team-agent/installer@latest install`.");
|
|
49
|
+
console.error("LOG:");
|
|
50
|
+
console.error(` INIT_CWD=${cwd}`);
|
|
51
|
+
console.error(` WSL_MNT_C=${wslHint}`);
|
|
52
|
+
console.error(` npmrc=${npmrcSummary}`);
|
|
53
|
+
console.error(` PATH_ENTRIES=${pathEntries}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function summarizeNpmrc(npmrcPath) {
|
|
57
|
+
if (!fs.existsSync(npmrcPath)) {
|
|
58
|
+
return `${npmrcPath} missing`;
|
|
59
|
+
}
|
|
60
|
+
const text = fs.readFileSync(npmrcPath, "utf8");
|
|
61
|
+
const hasPrefix = text
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.some((line) => /^\s*prefix\s*=/.test(line) && !/^\s*[#;]/.test(line));
|
|
64
|
+
return `${npmrcPath} present prefix=${hasPrefix ? "yes" : "no"}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isWslMntC(value) {
|
|
68
|
+
const normalized = value.replace(/\\/g, "/").toLowerCase();
|
|
69
|
+
return normalized.startsWith("/mnt/c/");
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
+
"postinstall": "node npm/bincheck.mjs",
|
|
23
24
|
"test": "PYTHONPATH=src python3 tests/run_tests.py",
|
|
24
25
|
"test:regression": "PYTHONPATH=src python3 scripts/run_regression_tests.py --iterations 3",
|
|
25
26
|
"test:rust": "cargo test --manifest-path crates/team-agent-core/Cargo.toml",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Bug As Artifact — Flow Convention (Reference)
|
|
2
|
+
|
|
3
|
+
This is a project-maintenance reference. NOT part of SKILL.md core. The SKILL.md surface keeps only ambient day-to-day operating rules; anything extra (release flow, this bug-flow convention, future Kanban infra) lives in `references/` and is read on demand.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The project maintainer reads bug docs as the primary把关/oversight loop. Going forward, the maintainer will also have a Web Kanban renderer that reads these files. This convention pins the path, format, and ownership so the Kanban (or any future view layer) has a stable single source.
|
|
8
|
+
|
|
9
|
+
This convention also doubles as a **flow file**: cr/te/developer/realistic-tester/spark all reference the same per-bug file during a slice; it is the intermediate record of the work, not a doc written specifically for the maintainer to read.
|
|
10
|
+
|
|
11
|
+
## Path strong-convention
|
|
12
|
+
|
|
13
|
+
All bug docs live in `.team/bugs/<3-digit-id>-<short-slug>.md` (project root relative). The numeric id matches the corresponding §N in `.team/artifacts/development-directions/team-agent-unattended-runtime-gaps.md` when one exists; bugs that don't have a gap-doc entry still get a sequential id (request the next from the leader at intake time).
|
|
14
|
+
|
|
15
|
+
`.team/bugs/README.md` carries the convention itself + index. The Web Kanban watches this directory.
|
|
16
|
+
|
|
17
|
+
## Format strong-convention (yaml + 5 sections)
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
---
|
|
21
|
+
id: bug-<NNN>
|
|
22
|
+
title: 短而具体的中文标题,普通人能看懂
|
|
23
|
+
status: open | in-progress | closed
|
|
24
|
+
severity: low | medium | high | critical
|
|
25
|
+
surfaced-by: 来源(谁/哪里/什么时候撞到的)
|
|
26
|
+
linked-gap: <N> # 可选,对应 gaps 文档 §N
|
|
27
|
+
linked-cr-verdict: <path> # 可选,如果走过 cr 设计审查
|
|
28
|
+
linked-contract: <path> # 可选,如果 te 已写契约
|
|
29
|
+
linked-commits: [<sha>, ...]
|
|
30
|
+
linked-release: <version-or-status>
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 背景
|
|
34
|
+
普通话,1-2 段,描述用户/场景/什么样的人会撞到这个 bug。不堆代码路径。
|
|
35
|
+
|
|
36
|
+
## 复现步骤
|
|
37
|
+
1 / 2 / 3 编号,让新接手的人能照着撞出来同样现象。
|
|
38
|
+
|
|
39
|
+
## 原因分析
|
|
40
|
+
讲【为什么】,原因链:用户做了什么 → 框架代码里发生了什么 → 最终为什么得到坏结果。
|
|
41
|
+
可以引用 file:line 但必须伴随中文解释。
|
|
42
|
+
|
|
43
|
+
## 修改方案
|
|
44
|
+
怎么修。修法形态、涉及哪些文件、有什么 cr constraint。还没决定就写 "待 cr 设计审查"。
|
|
45
|
+
|
|
46
|
+
## 解决现状
|
|
47
|
+
表格列出 pipeline 各阶段(用户撞到 / 记入文档 / cr verdict / te 契约 / 实现 / 测试 / E2E / ship)的 status,带时间。
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Ownership: who writes what
|
|
51
|
+
|
|
52
|
+
| 角色 | 在 bug 文件里做什么 |
|
|
53
|
+
|---|---|
|
|
54
|
+
| **leader(this Claude)** | 创建文件、汇总 cr/te/developer 各阶段产出到「解决现状」、推进 status、closed 时填 linked-release |
|
|
55
|
+
| **agent(cr/te/developer/...)** | **读** 文件作背景上下文;**不直接写**(各自有专属产物位置:cr verdict 文件 / te 契约 / commit msg / spark findings) |
|
|
56
|
+
| **maintainer(user)** | 任意时间扫读全目录把关方向,纠正 leader 错的推进 |
|
|
57
|
+
|
|
58
|
+
**为什么不让 agent 直接写**:多人写同一文件冲突,且各 agent 自己的产物有更结构化的位置。leader 是单一汇总点,确保 bug 文件版本一致 + 普通话风格统一。
|
|
59
|
+
|
|
60
|
+
## Plain-language standard
|
|
61
|
+
|
|
62
|
+
跟 CONSTITUTION 风格一致:【短而硬】+ 【普通人能读懂】。
|
|
63
|
+
|
|
64
|
+
**不允许**:堆英文术语 / file:line 大段引用没注释 / 内部 jargon 把人挡在外面 / 给读者抛专业概念不解释。
|
|
65
|
+
|
|
66
|
+
**允许**:必要的 commit SHA / 文件名 / event 名,但每次出现都伴随中文解释。
|
|
67
|
+
|
|
68
|
+
## When to spawn a new bug file
|
|
69
|
+
|
|
70
|
+
任何一次新的友 bug / 真机 halt / cr/te/developer 工作过程中浮现的新结构性问题 → 立即创建 `.team/bugs/<next-id>-<slug>.md` 作为这次工作的承载文件,把所有产物 link 进来。
|
|
71
|
+
|
|
72
|
+
不要让一个 bug 没有自己的文件就开始派工。
|
|
73
|
+
|
|
74
|
+
## Relationship to SKILL.md
|
|
75
|
+
|
|
76
|
+
SKILL.md 保留团队 CLI 操作必备(quick-start / send / status / restart / shutdown / failure rules / worker protocol)。所有【流程/治理】层面的扩展(release flow / bug flow / 看板基础)都从 SKILL.md 分出到 references/,以保持 SKILL 主体的【最关键】定位。
|
|
77
|
+
|
|
78
|
+
## Relationship to future Web Kanban
|
|
79
|
+
|
|
80
|
+
`.team/bugs/` 的路径+格式强约是为 Kanban renderer 准备的单一来源:Kanban 监听该目录的 frontmatter,渲染三栏(open / in-progress / closed)看板,每张卡片展示 title + 状态 + linked-commits + plain-language 摘要,点开看完整正文。Kanban 是 view layer,不是 control plane(§1.5 / §2.1.MUST-NOT-1 不变)。
|
|
81
|
+
|
|
82
|
+
未来 Kanban 实现 slice 由用户拍优先级时启动(framework-innovation-directions §5 Slice 2)。
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Legacy reverse-scan tmux helpers retained for compatibility with
|
|
2
|
+
existing receiver-discovery / takeover / claim-leader fallback paths.
|
|
3
|
+
|
|
4
|
+
0.2.6 main slice (Family A) introduced
|
|
5
|
+
``team_agent.leader_binding.bind_owner_from_caller_pane`` as the positive
|
|
6
|
+
source for owner identity. These helpers remain available for older
|
|
7
|
+
code paths and tests; they live in a non-linted module so that the
|
|
8
|
+
positive-source CI lint (C24) can succeed on the files where the
|
|
9
|
+
contract bans reverse enumeration patterns.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from team_agent.runtime import TMUX_PANE_FORMAT, run_cmd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _tmux_current_client_pane_info() -> dict[str, str] | None:
|
|
22
|
+
proc = run_cmd(["tmux", "display-message", "-p", "-F", TMUX_PANE_FORMAT], timeout=5)
|
|
23
|
+
if proc.returncode != 0:
|
|
24
|
+
return None
|
|
25
|
+
return _parse_tmux_pane_info(proc.stdout.strip())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _tmux_list_panes() -> list[dict[str, str]]:
|
|
29
|
+
proc = run_cmd(["tmux", "list-panes", "-a", "-F", TMUX_PANE_FORMAT], timeout=5)
|
|
30
|
+
if proc.returncode != 0:
|
|
31
|
+
return []
|
|
32
|
+
return [pane for line in proc.stdout.splitlines() if (pane := _parse_tmux_pane_info(line))]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _tmux_pane_info(target: str | None) -> dict[str, str] | None:
|
|
36
|
+
if not target:
|
|
37
|
+
return None
|
|
38
|
+
proc = run_cmd(["tmux", "display-message", "-p", "-t", target, "-F", TMUX_PANE_FORMAT], timeout=5)
|
|
39
|
+
if proc.returncode != 0:
|
|
40
|
+
return None
|
|
41
|
+
return _parse_tmux_pane_info(proc.stdout.strip())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_tmux_pane_info(line: str) -> dict[str, str] | None:
|
|
45
|
+
parts = line.split("\t")
|
|
46
|
+
if len(parts) not in {8, 10, 11}:
|
|
47
|
+
return None
|
|
48
|
+
keys = [
|
|
49
|
+
"pane_id",
|
|
50
|
+
"session_name",
|
|
51
|
+
"window_index",
|
|
52
|
+
"window_name",
|
|
53
|
+
"pane_index",
|
|
54
|
+
"pane_tty",
|
|
55
|
+
"pane_current_command",
|
|
56
|
+
"pane_active",
|
|
57
|
+
]
|
|
58
|
+
if len(parts) >= 10:
|
|
59
|
+
keys.extend(["pane_current_path", "session_attached"])
|
|
60
|
+
if len(parts) == 11:
|
|
61
|
+
keys.append("pane_in_mode")
|
|
62
|
+
return dict(zip(keys, parts))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _infer_active_tmux_pane(provider: str) -> dict[str, str] | None:
|
|
66
|
+
from team_agent.messaging.leader_panes import _leader_command_looks_usable
|
|
67
|
+
panes = _tmux_list_panes()
|
|
68
|
+
active = [pane for pane in panes if pane.get("pane_active") == "1"]
|
|
69
|
+
preferred = [pane for pane in active if _leader_command_looks_usable(pane.get("pane_current_command", ""), provider)]
|
|
70
|
+
if len(preferred) == 1:
|
|
71
|
+
return preferred[0]
|
|
72
|
+
if len(active) == 1:
|
|
73
|
+
return active[0]
|
|
74
|
+
if preferred:
|
|
75
|
+
return preferred[0]
|
|
76
|
+
return active[0] if active else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _infer_workspace_tmux_pane(provider: str, workspace: Path) -> dict[str, Any]:
|
|
80
|
+
from team_agent.messaging.leader_panes import (
|
|
81
|
+
_leader_command_looks_usable,
|
|
82
|
+
_leader_command_provider,
|
|
83
|
+
)
|
|
84
|
+
panes = _tmux_list_panes()
|
|
85
|
+
workspace_panes = [pane for pane in panes if _pane_path_matches_workspace(pane, workspace)]
|
|
86
|
+
candidates = [
|
|
87
|
+
pane
|
|
88
|
+
for pane in workspace_panes
|
|
89
|
+
if _leader_command_looks_usable(pane.get("pane_current_command", ""), provider)
|
|
90
|
+
or _leader_command_provider(pane.get("pane_current_command", "")) is not None
|
|
91
|
+
]
|
|
92
|
+
if not candidates:
|
|
93
|
+
return {"status": "missing", "workspace_panes": workspace_panes}
|
|
94
|
+
ranked = sorted(candidates, key=lambda item: _leader_pane_rank(item, provider), reverse=True)
|
|
95
|
+
best_rank = _leader_pane_rank(ranked[0], provider)
|
|
96
|
+
best = [pane for pane in ranked if _leader_pane_rank(pane, provider) == best_rank]
|
|
97
|
+
if len(best) == 1:
|
|
98
|
+
return {"status": "ok", "pane": best[0], "candidates": candidates}
|
|
99
|
+
return {"status": "ambiguous", "candidates": best}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pane_is_usable_leader(pane: dict[str, str], provider: str, workspace: Path | None) -> bool:
|
|
103
|
+
from team_agent.messaging.leader_panes import _leader_command_looks_usable, _leader_command_provider
|
|
104
|
+
command = pane.get("pane_current_command", "")
|
|
105
|
+
if not _leader_command_looks_usable(command, provider) and _leader_command_provider(command) is None:
|
|
106
|
+
return False
|
|
107
|
+
if workspace is not None and not _pane_path_matches_workspace(pane, workspace):
|
|
108
|
+
return False
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pane_path_matches_workspace(pane: dict[str, str], workspace: Path) -> bool:
|
|
113
|
+
current_path = pane.get("pane_current_path")
|
|
114
|
+
if not current_path:
|
|
115
|
+
return False
|
|
116
|
+
return os.path.realpath(current_path) == os.path.realpath(str(workspace.resolve()))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _leader_pane_rank(pane: dict[str, str], provider: str) -> tuple[int, int, int]:
|
|
120
|
+
from team_agent.messaging.leader_panes import _leader_command_is_exact
|
|
121
|
+
return (
|
|
122
|
+
_tmux_truthy(pane.get("session_attached", "")),
|
|
123
|
+
1 if pane.get("pane_active") == "1" else 0,
|
|
124
|
+
1 if _leader_command_is_exact(pane.get("pane_current_command", ""), provider) else 0,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _tmux_truthy(value: str) -> int:
|
|
129
|
+
try:
|
|
130
|
+
return 1 if int(value) > 0 else 0
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
return 1 if value and value != "0" else 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _format_leader_pane_candidates(candidates: list[dict[str, str]]) -> str:
|
|
136
|
+
compact = []
|
|
137
|
+
for pane in candidates[:5]:
|
|
138
|
+
compact.append(
|
|
139
|
+
"{pane_id} session={session_name} pane={window_index}.{pane_index} "
|
|
140
|
+
"cmd={pane_current_command} cwd={pane_current_path} active={pane_active}".format(**pane)
|
|
141
|
+
)
|
|
142
|
+
suffix = "" if len(candidates) <= 5 else f" ... +{len(candidates) - 5} more"
|
|
143
|
+
return "candidates: " + "; ".join(compact) + suffix
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _resolve_leader_pane(
|
|
147
|
+
pane: str | None,
|
|
148
|
+
provider: str,
|
|
149
|
+
workspace: Path | None = None,
|
|
150
|
+
require_current: bool = False,
|
|
151
|
+
) -> tuple[dict[str, str], str]:
|
|
152
|
+
from team_agent.errors import RuntimeError as _RuntimeError
|
|
153
|
+
if pane:
|
|
154
|
+
pane_info = _tmux_pane_info(pane)
|
|
155
|
+
if not pane_info:
|
|
156
|
+
raise _RuntimeError(f"tmux pane not found: {pane}")
|
|
157
|
+
return pane_info, "explicit_pane"
|
|
158
|
+
pane_info = _tmux_current_client_pane_info()
|
|
159
|
+
if pane_info and _pane_is_usable_leader(pane_info, provider, workspace):
|
|
160
|
+
return pane_info, "current_client"
|
|
161
|
+
if workspace is not None:
|
|
162
|
+
workspace_match = _infer_workspace_tmux_pane(provider, workspace)
|
|
163
|
+
if workspace_match["status"] == "ok":
|
|
164
|
+
return workspace_match["pane"], "workspace_pane_scan"
|
|
165
|
+
if workspace_match["status"] == "ambiguous":
|
|
166
|
+
raise _RuntimeError(
|
|
167
|
+
"multiple tmux leader panes match this workspace; pass --pane explicitly. "
|
|
168
|
+
+ _format_leader_pane_candidates(workspace_match["candidates"])
|
|
169
|
+
)
|
|
170
|
+
if require_current:
|
|
171
|
+
details = ""
|
|
172
|
+
if pane_info:
|
|
173
|
+
details = (
|
|
174
|
+
f" Current tmux client points at pane {pane_info.get('pane_id')} "
|
|
175
|
+
f"command={pane_info.get('pane_current_command')!r} "
|
|
176
|
+
f"cwd={pane_info.get('pane_current_path')!r}, not a usable pane for this workspace."
|
|
177
|
+
)
|
|
178
|
+
raise _RuntimeError(
|
|
179
|
+
"Team Agent could not locate a tmux-managed leader pane for this workspace. "
|
|
180
|
+
"Run quick-start from the visible tmux-managed leader pane, pass --pane explicitly, "
|
|
181
|
+
"or use `team-agent codex`/`team-agent claude` as a convenience fallback."
|
|
182
|
+
+ details
|
|
183
|
+
)
|
|
184
|
+
if pane_info and workspace is None:
|
|
185
|
+
return pane_info, "current_client"
|
|
186
|
+
pane_info = _infer_active_tmux_pane(provider)
|
|
187
|
+
if pane_info:
|
|
188
|
+
return pane_info, "active_pane_scan"
|
|
189
|
+
raise _RuntimeError("could not infer a tmux leader pane; pass --pane <pane_id>")
|
|
@@ -23,6 +23,95 @@ def emit(result: Any, as_json: bool) -> None:
|
|
|
23
23
|
print(result)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def consume_leader_inbox_summary(workspace: Path, *, budget: int = 500) -> str | None:
|
|
27
|
+
runtime_dir = workspace / ".team" / "runtime"
|
|
28
|
+
inbox_path = runtime_dir / "leader-inbox.log"
|
|
29
|
+
cursor_path = runtime_dir / "leader-inbox.cursor"
|
|
30
|
+
if not inbox_path.exists():
|
|
31
|
+
return None
|
|
32
|
+
try:
|
|
33
|
+
size = inbox_path.stat().st_size
|
|
34
|
+
offset = int(cursor_path.read_text(encoding="utf-8").strip() or "0") if cursor_path.exists() else 0
|
|
35
|
+
except (OSError, ValueError):
|
|
36
|
+
offset = 0
|
|
37
|
+
size = 0
|
|
38
|
+
if offset < 0 or offset > size:
|
|
39
|
+
offset = 0
|
|
40
|
+
if offset == size:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
with inbox_path.open("rb") as handle:
|
|
44
|
+
handle.seek(offset)
|
|
45
|
+
raw = handle.read()
|
|
46
|
+
end = handle.tell()
|
|
47
|
+
except OSError:
|
|
48
|
+
return None
|
|
49
|
+
text = raw.decode("utf-8", errors="replace")
|
|
50
|
+
try:
|
|
51
|
+
cursor_path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
cursor_path.write_text(str(end), encoding="utf-8")
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
return _leader_inbox_summary(text, budget=budget)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _leader_inbox_summary(text: str, *, budget: int) -> str | None:
|
|
59
|
+
entries = _leader_inbox_entries(text)
|
|
60
|
+
if not entries:
|
|
61
|
+
return None
|
|
62
|
+
hint = "team-agent inbox leader"
|
|
63
|
+
lines = [f"Leader inbox: {len(entries)} new fallback entr{'y' if len(entries) == 1 else 'ies'}"]
|
|
64
|
+
truncated = False
|
|
65
|
+
for entry in entries:
|
|
66
|
+
item = _leader_inbox_entry_title(entry)[:80]
|
|
67
|
+
candidate = "\n".join([*lines, f"- {item}", f"Hint: {hint}"])
|
|
68
|
+
if len(candidate) > budget:
|
|
69
|
+
truncated = True
|
|
70
|
+
break
|
|
71
|
+
lines.append(f"- {item}")
|
|
72
|
+
footer = f"Hint: {hint}"
|
|
73
|
+
if truncated or len(lines) - 1 < len(entries):
|
|
74
|
+
footer = f"Truncated: more fallback entries available; run {hint}"
|
|
75
|
+
summary = "\n".join([*lines, footer])
|
|
76
|
+
if len(summary) > budget:
|
|
77
|
+
keep = max(0, budget - len(footer) - 6)
|
|
78
|
+
body = "\n".join(lines)[:keep].rstrip()
|
|
79
|
+
summary = "\n".join([f"{body} ...", footer])
|
|
80
|
+
return summary
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _leader_inbox_entry_title(entry: str) -> str:
|
|
84
|
+
lines = [line.strip() for line in entry.splitlines() if line.strip()]
|
|
85
|
+
content_lines: list[str] = []
|
|
86
|
+
for line in lines:
|
|
87
|
+
if line.startswith("[") and ("fallback" in line or "team-agent-token" in line):
|
|
88
|
+
continue
|
|
89
|
+
if line.startswith("Team Agent"):
|
|
90
|
+
continue
|
|
91
|
+
if line.startswith(("Message id:", "Task id:", "From:", "To:", "Requires ack:", "Artifacts:")):
|
|
92
|
+
continue
|
|
93
|
+
content_lines.append(line)
|
|
94
|
+
if content_lines:
|
|
95
|
+
return " ".join(" ".join(line.split()) for line in content_lines)
|
|
96
|
+
return " ".join(entry.split())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _leader_inbox_entries(text: str) -> list[str]:
|
|
100
|
+
blocks: list[str] = []
|
|
101
|
+
current: list[str] = []
|
|
102
|
+
for line in text.splitlines():
|
|
103
|
+
if line.startswith("[") and "fallback" in line and current:
|
|
104
|
+
blocks.append("\n".join(current).strip())
|
|
105
|
+
current = [line]
|
|
106
|
+
elif line.strip():
|
|
107
|
+
current.append(line)
|
|
108
|
+
if current:
|
|
109
|
+
blocks.append("\n".join(current).strip())
|
|
110
|
+
if blocks:
|
|
111
|
+
return blocks
|
|
112
|
+
return [line.strip() for line in text.splitlines() if line.strip()]
|
|
113
|
+
|
|
114
|
+
|
|
26
115
|
def _workspace_from_args(args: argparse.Namespace) -> Path:
|
|
27
116
|
return Path(getattr(args, "workspace", ".")).resolve()
|
|
28
117
|
|
|
@@ -61,6 +61,8 @@ from team_agent.cli.helpers import (
|
|
|
61
61
|
_emit_cli_error,
|
|
62
62
|
_leader_launcher_args,
|
|
63
63
|
_provider_args,
|
|
64
|
+
_workspace_from_args,
|
|
65
|
+
consume_leader_inbox_summary,
|
|
64
66
|
emit,
|
|
65
67
|
)
|
|
66
68
|
|
|
@@ -481,6 +483,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
481
483
|
except Exception as exc:
|
|
482
484
|
_emit_cli_error(exc, args)
|
|
483
485
|
raise SystemExit(1)
|
|
486
|
+
summary = consume_leader_inbox_summary(_workspace_from_args(args))
|
|
487
|
+
if summary:
|
|
488
|
+
print(summary, file=sys.stderr if getattr(args, "json", False) else sys.stdout)
|
|
484
489
|
emit(result, getattr(args, "json", False))
|
|
485
490
|
if isinstance(result, dict) and result.get("ok") is False:
|
|
486
491
|
raise SystemExit(1)
|
|
@@ -121,7 +121,7 @@ def prepare_quick_start_team(agents_dir: Path, workspace: Path, name: str | None
|
|
|
121
121
|
team_source = agents_dir / "TEAM.md"
|
|
122
122
|
role_docs = [path for path in sorted(agents_dir.glob("*.md")) if path.name != "TEAM.md"] if agents_dir.is_dir() else []
|
|
123
123
|
if not role_docs:
|
|
124
|
-
raise RuntimeError(f"{agents_dir}:
|
|
124
|
+
raise RuntimeError(f"{agents_dir}: 该目录缺角色定义文件 — 先用 'team-agent quick-start <roles-dir>' 从 role .md 创建一个新 team")
|
|
125
125
|
team_dir = workspace / ".team" / (_safe_snapshot_name(team_id) if team_id else "current")
|
|
126
126
|
target_agents = team_dir / "agents"
|
|
127
127
|
target_profiles = team_dir / "profiles"
|