Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@
one-level family graph, and an SSE event stream with `after=` resume.
Hidden tabs pause; SSE failure falls back to the existing 5s poll.
Workspace detail shows the family tree and live event panes. Copy
buttons emit dry-run CLI only. Browser stays read-only; P2 channel
POST and P3 artifacts are not implemented. Family badges do not claim
buttons emit dry-run CLI only. Family badges do not claim
cleanliness or origin binding. Event `facts` stay IDs, enums, counts,
short hashes, or reason codes. HMAC `after=` binds a row digest so a
replaced same-seq row is `EVENT_CURSOR_INVALID`.
- Console P2: overlay family channel at
`.dyro/families/<parent>/channel.jsonl` plus `acks.json`, dual-written
with a `signal` event under the same lock. CLI `line post` / `inbox` /
`ack` stay under `dyro line`. `next --format json` adds read-only
`family_unacked` and never emits `line post` or `line ack --yes`.
Console channel pane shows full family history; the browser POST
accepts only `decision` / `contract` / `ack` as `operator`. Artifact
bytes stay closed (P3). `/dyro-line-family` still does not post.
CLI `--to operator` uses the same default family as a broadcast
(`parent` or sender), not `F(sender)`. Channel pairing and ack treat
`msg_N` as per-family; HTTP ack is bound to the URL family, and CLI
`line ack` fail-closes when the same id exists in more than one family.
- User slash Skill `dyro-line-family` (`/dyro-line-family`) preflights
`line spawn` / `line merge` / `line sync` and prints one `--yes` command
for the human. It does not execute the mutation, invent `--push`, or
Expand Down
4 changes: 3 additions & 1 deletion docs/designs/console-v2-live-family-signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ meta `surfaces` 增加 `events`(P1)与 `families`(P2)。页面按能力

### P1 · 事件 + 图

P1 已在本 PR 落地(事件尾、`parent` 投影、一层家族图、SSE `after=`)。P2 / P3 仍未实现。
P1 已在本 PR 落地(事件尾、`parent` 投影、一层家族图、SSE `after=`)。P3 仍未实现。

- 写入并读取 `.dyro/events.jsonl`。
- line DTO 投影 `parent`。
Expand All @@ -406,6 +406,8 @@ P1 已在本 PR 落地(事件尾、`parent` 投影、一层家族图、SSE `af

### P2 · 家族频道 + 人类模块

P2 已落地:`channel.jsonl` / `acks`、`line post|inbox|ack`、`next family_unacked`、人类 POST `decision|contract|ack`。

- 计算 `F(P)`,写入 `channel.jsonl` 与对应 `signal` 事件。
- CLI:`line post` / `inbox` / `ack`。
- `next` 与控制面读取未 ack。
Expand Down
170 changes: 170 additions & 0 deletions src/dyro/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
)
from .evidence import build_execution_bundle, unpack_execution_bundle
from .errors import DyroError, ValidationError
from .families import ack_channel_message, family_unacked, list_inbox, post_channel_message
from .home import (
HomeTool,
_record_for_root,
Expand Down Expand Up @@ -390,6 +391,36 @@ def _require_objective_yes(args: argparse.Namespace, label: str) -> None:
)


def _require_overlay_yes(args: argparse.Namespace, label: str) -> None:
if not args.yes and not args.dry_run:
raise DyroError(
f"{label} 会写入 overlay 家族信号;请先使用 --dry-run 检查,再加 --yes 执行"
)


def _family_unacked_fields(config: Config) -> dict[str, object]:
try:
payload = family_unacked(config)
except (DyroError, OSError, UnicodeError):
payload = {"count": 0, "kind": "", "family": "", "summary": ""}
if not isinstance(payload, dict):
payload = {"count": 0, "kind": "", "family": "", "summary": ""}
return {"family_unacked": payload}


def _print_family_unacked_attention(config: Config) -> None:
payload = _family_unacked_fields(config)["family_unacked"]
if isinstance(payload, dict) and payload.get("count"):
print("家族频道有未读信号")


def _inbox_viewer(config: Config) -> str:
try:
return resolve_line(config, interactive=False).id
except (DyroError, ValidationError):
return ""


def _objective_contract_from_args(args: argparse.Namespace, config: Config) -> str:
if args.file:
path = Path(args.file).expanduser()
Expand Down Expand Up @@ -2588,6 +2619,7 @@ def cmd_next(args: argparse.Namespace) -> None:
diagnostic_commands=[_briefing_command(args, config, "doctor")],
mutation_available=bootstrap_applicable,
findings=findings,
**_family_unacked_fields(config),
**_next_push_fields(config),
)
return
Expand All @@ -2600,6 +2632,7 @@ def cmd_next(args: argparse.Namespace) -> None:
"缺失仓库均已配置 remote,可运行:"
+ _briefing_command(args, config, "bootstrap", "--yes")
)
_print_family_unacked_attention(config)
_print_push_disclosure(config)
return
lines = list_lines(config, read_budget=budget)
Expand All @@ -2612,10 +2645,12 @@ def cmd_next(args: argparse.Namespace) -> None:
summary="Profile 已就绪,但还没有开发线。",
commands=[command],
mutation_available=True,
**_family_unacked_fields(config),
**_next_push_fields(config),
)
return
print(f"Profile 已就绪,但还没有开发线。下一步:{command}")
_print_family_unacked_attention(config)
_print_push_disclosure(config)
return
if not config.adapters and not installed_launchable_presets():
Expand All @@ -2627,6 +2662,7 @@ def cmd_next(args: argparse.Namespace) -> None:
commands=[],
mutation_available=False,
required_inputs=["agent_id", "agent_command"],
**_family_unacked_fields(config),
**_next_push_fields(config),
)
return
Expand All @@ -2635,6 +2671,7 @@ def cmd_next(args: argparse.Namespace) -> None:
"安装本机 Agent 后运行 dyro start,或 "
+ _scoped_command(args, config, "agent", "add", "<id>", "--command", "…")
)
_print_family_unacked_attention(config)
_print_push_disclosure(config)
return
briefing, diagnostic_commands = _workspace_ready_briefing(
Expand All @@ -2655,16 +2692,19 @@ def cmd_next(args: argparse.Namespace) -> None:
_doctor_finding_payload(item, include_paths=False)
for item in missing_origin_failures
]
payload.update(_family_unacked_fields(config))
payload.update(_next_push_fields(config))
_print_control_plane_json("next_step", **payload)
return
for finding in missing_origin_failures:
_print_doctor_finding(finding)
if briefing is None:
print("工作区已就绪。可用 dyro start 打开本机已安装的编码工具。")
_print_family_unacked_attention(config)
_print_push_disclosure(config)
return
print(render_briefing_text(briefing))
_print_family_unacked_attention(config)
_print_push_disclosure(config)


Expand Down Expand Up @@ -2791,6 +2831,70 @@ def cmd_line_sync(args: argparse.Namespace) -> None:
)


def cmd_line_post(args: argparse.Namespace) -> None:
config = _config(args)
_require_overlay_yes(args, "写入家族频道")
result = post_channel_message(
config,
sender=args.sender,
kind=args.kind,
body=args.body or "",
recipient=args.to or "",
family=args.family or "",
dry_run=args.dry_run,
)
if args.format == "json":
payload = dict(result)
payload["channel_kind"] = payload.pop("kind", "")
_print_control_plane_json("line_post", **payload)
return
target = result["to"] or "家族广播"
prefix = "DRY RUN: " if args.dry_run else ""
print(
f"{prefix}已从 {result['from']} 向 {target} 发送 {result['kind']} "
f"{result['id']}(家族 {result['family']})"
)


def cmd_line_inbox(args: argparse.Namespace) -> None:
config = _config(args)
inbox = list_inbox(
config,
family=args.family or "",
viewer=_inbox_viewer(config),
unacked=args.unacked,
)
if args.format == "json":
_print_control_plane_json("line_inbox", **inbox)
return
messages = inbox["messages"]
print(
f"家族 {inbox['family']} · 查看者 {inbox['viewer']} · 未读 {inbox['unacked']}"
)
if not messages:
print("没有可展示的家族信号")
return
for item in messages:
target = item["to"] or "广播"
flag = "未读" if not item["acked"] else "已读"
print(
f"{item['id']:10} {item['kind']:10} {item['from']} → {target} {flag} {item['body']}"
)


def cmd_line_ack(args: argparse.Namespace) -> None:
config = _config(args)
_require_overlay_yes(args, "确认已读家族信号")
result = ack_channel_message(
config, args.id, family=args.family or "", dry_run=args.dry_run
)
if args.format == "json":
_print_control_plane_json("line_ack", **result)
return
prefix = "DRY RUN: " if args.dry_run else ""
print(f"{prefix}已确认已读 {result['id']}(家族 {result['family']})")


def cmd_hotfix_create(args: argparse.Namespace) -> None:
_create_line(args, "hotfix")

Expand Down Expand Up @@ -4824,6 +4928,72 @@ def build_parser() -> argparse.ArgumentParser:
)
line_sync.add_argument("--yes", action="store_true")
line_sync.set_defaults(func=cmd_line_sync)
line_post = line_sub.add_parser("post", help="向一层家族频道追加 overlay 信号")
line_post.add_argument("sender", metavar="LINE", help="发送者线 id;人类用 operator")
line_post.add_argument(
"--kind",
required=True,
choices=(
"contract",
"blocked",
"shipped",
"ask_sync",
"decision",
"artifact",
"retract",
),
help="频道 kind;operator 只能发 decision 或 contract",
)
line_post.add_argument("--to", default="", help="定向接收者;省略则为家族广播")
line_post.add_argument("--body", default="", help="正文;retract 时填被撤回的 msg id")
line_post.add_argument(
"--family",
default="",
help="家族父线;operator 广播时必填",
)
line_post.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
default=argparse.SUPPRESS,
help="只打印计划,不写频道或事件(兼容全局 --dry-run)",
)
line_post.add_argument("--yes", action="store_true")
line_post.add_argument("--format", choices=("text", "json"), default="text")
line_post.set_defaults(func=cmd_line_post)
line_inbox = line_sub.add_parser("inbox", help="读取一层家族频道可见行")
line_inbox.add_argument("--family", default="", help="家族父线;省略则看当前线")
line_inbox.add_argument(
"--unacked",
action="store_true",
help="只列出操作者尚未 ack 的行",
)
line_inbox.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
default=argparse.SUPPRESS,
help="只读;不写频道(兼容全局 --dry-run)",
)
line_inbox.add_argument("--format", choices=("text", "json"), default="text")
line_inbox.set_defaults(func=cmd_line_inbox)
line_ack = line_sub.add_parser("ack", help="将一条家族信号标为人类已读")
line_ack.add_argument("id", help="频道行 id,例如 msg_1")
line_ack.add_argument(
"--family",
default="",
help="家族父线;同号出现在多个家族时必填",
)
line_ack.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
default=argparse.SUPPRESS,
help="只打印计划,不写 ack(兼容全局 --dry-run)",
)
line_ack.add_argument("--yes", action="store_true")
line_ack.add_argument("--format", choices=("text", "json"), default="text")
line_ack.set_defaults(func=cmd_line_ack)

hotfix = sub.add_parser("hotfix", help="生产 Hotfix 开发线")
hotfix_sub = hotfix.add_subparsers(dest="hotfix_command", required=True)
Expand Down
20 changes: 20 additions & 0 deletions src/dyro/console/_inspect_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ def _decode_request(value: str) -> dict[str, object]:
"alias",
"after",
"parent",
"filter",
"target_root",
}:
raise ConsoleOverviewError("OVERVIEW_UNAVAILABLE")
Expand Down Expand Up @@ -339,6 +340,25 @@ def main(argv: list[str] | None = None) -> int:
if not isinstance(parent, str):
raise ConsoleOverviewError("FAMILY_PARENT_INVALID")
payload = service.family(alias, parent)
elif operation == "channel":
alias = request.get("alias")
parent = request.get("parent")
if not isinstance(alias, str):
raise ConsoleOverviewError("WORKSPACE_ALIAS_INVALID")
if not isinstance(parent, str):
raise ConsoleOverviewError("FAMILY_PARENT_INVALID")
after = request.get("after")
if after is not None and not isinstance(after, str):
raise ConsoleOverviewError("CHANNEL_CURSOR_INVALID")
filter_text = request.get("filter")
if filter_text is not None and not isinstance(filter_text, str):
raise ConsoleOverviewError("CHANNEL_FILTER_INVALID")
limit = request.get("limit", 50)
if type(limit) is not int:
raise ConsoleOverviewError("CHANNEL_LIMIT_INVALID")
payload = service.channel(
alias, parent, after=after, filter=filter_text, limit=limit
)
else:
raise ConsoleOverviewError("OVERVIEW_UNAVAILABLE")
return _response(payload)
Expand Down
8 changes: 4 additions & 4 deletions src/dyro/console/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ class ConsoleAsset:
),
"app.js": (
"text/javascript; charset=utf-8",
"5ee284b3cd590ba114a64b1994ac79de64b7376bd38b5163f74176b704ac0113",
47380,
"194ca77cdfac96539dd391f43d6d14a5e15bf594ec21a280c886a21536124c23",
60441,
),
"styles.css": (
"text/css; charset=utf-8",
"dbf956c627803fe47ad910ec79a6a0763e6a40c993fa2e96e5ec79739fcd3432",
14676,
"810b196f7e72418d7253c1f2aa490f82fbd51a98a12d2db2d51e8303b4632f07",
15492,
),
}

Expand Down
Loading