From a99566d775c83fdd42af5f8294c8d8a01e28e648 Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:01:42 +0000 Subject: [PATCH 1/7] =?UTF-8?q?fix(=E6=9B=B4=E6=96=B0):=20replace=20uv-too?= =?UTF-8?q?l=20exact=20pins=20via=20install=20--force?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uv tool upgrade cannot satisfy dyro== when the receipt already pins specifier==. Plan uv-managed updates with uv tool install --force so the pin is replaced. --- src/dyro/updates.py | 3 +- tests/test_updates.py | 100 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/dyro/updates.py b/src/dyro/updates.py index 4dcfceb..9cb5fa4 100644 --- a/src/dyro/updates.py +++ b/src/dyro/updates.py @@ -337,7 +337,8 @@ def build_update_plan( ( uv, "tool", - "upgrade", + "install", + "--force", "--default-index", PYPI_SIMPLE_URL, "--no-config", diff --git a/tests/test_updates.py b/tests/test_updates.py index e09d67b..994d3b9 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -245,18 +245,23 @@ def which(name: str) -> str | None: pip_available=True, ) + self.assertEqual(uv.manager, "uv tool") + self.assertEqual(uv.scope, "当前 uv tool 隔离环境") self.assertEqual( uv.argv, ( "/bin/uv", "tool", - "upgrade", + "install", + "--force", "--default-index", "https://pypi.org/simple", "--no-config", "dyro==0.5.6", ), ) + self.assertNotIn("upgrade", uv.argv) + self.assertEqual(uv.constraint, "") self.assertEqual( pipx.argv, ( @@ -283,6 +288,99 @@ def which(name: str) -> str | None: ), ) self.assertNotIn("sh", uv.argv) + self.assertEqual(pipx.manager, "pipx") + self.assertEqual(pip.manager, "pip") + + def test_uv_tool_plan_replaces_exact_pin_via_install_force(self) -> None: + commands = {"uv": "/bin/uv", "pipx": "/bin/pipx"} + + def which(name: str) -> str | None: + return commands.get(name) + + prefixes = ( + "/home/me/.local/share/uv/tools/dyro", + "/home/me/.local/share/uv/tools/dyro/extra", + r"C:\Users\me\AppData\Roaming\uv\tools\dyro", + ) + for prefix in prefixes: + with self.subTest(prefix=prefix): + plan = build_update_plan( + "0.7.12", + prefix=prefix, + executable="/tool/bin/python", + which=which, + editable=False, + pip_available=True, + ) + self.assertEqual(plan.manager, "uv tool") + self.assertEqual(plan.scope, "当前 uv tool 隔离环境") + self.assertEqual(plan.argv[0], "/bin/uv") + self.assertEqual(plan.argv[1:4], ("tool", "install", "--force")) + self.assertNotIn("upgrade", plan.argv) + self.assertEqual( + plan.argv[4:], + ( + "--default-index", + "https://pypi.org/simple", + "--no-config", + "dyro==0.7.12", + ), + ) + self.assertEqual(plan.constraint, "") + + pipx = build_update_plan( + "0.7.12", + prefix="/home/me/.local/pipx/venvs/dyro", + executable="/tool/bin/python", + which=which, + editable=False, + ) + self.assertEqual(pipx.manager, "pipx") + self.assertEqual( + pipx.argv, + ( + "/bin/pipx", + "upgrade", + "--index-url", + "https://pypi.org/simple", + "dyro", + ), + ) + self.assertEqual(pipx.constraint, "dyro==0.7.12") + + pip = build_update_plan( + "0.7.12", + prefix="/home/me/venv", + executable="/home/me/venv/bin/python", + which=lambda _: None, + editable=False, + pip_available=True, + ) + self.assertEqual(pip.manager, "pip") + self.assertIn("--upgrade", pip.argv) + self.assertNotIn("--force", pip.argv) + + uv_pip = build_update_plan( + "0.7.12", + prefix="/home/me/venv", + base_prefix="/usr/local", + executable="/home/me/venv/bin/python", + which=lambda name: "/bin/uv" if name == "uv" else None, + editable=False, + pip_available=False, + ) + self.assertEqual(uv_pip.manager, "uv pip") + self.assertEqual(uv_pip.argv[1:3], ("pip", "install")) + self.assertIn("--upgrade", uv_pip.argv) + self.assertNotIn("--force", uv_pip.argv) + + with self.assertRaisesRegex(DyroError, "找不到 uv"): + build_update_plan( + "0.7.12", + prefix="/home/me/.local/share/uv/tools/dyro", + which=lambda _: None, + editable=False, + ) def test_refuses_to_replace_an_editable_source_checkout(self) -> None: with self.assertRaisesRegex(DyroError, "editable"): From a75229baf0b25e157afd0a87710a5b09d4140405 Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:01:42 +0000 Subject: [PATCH 2/7] =?UTF-8?q?chore(=E5=8F=91=E5=B8=83):=200.7.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship the uv-tool exact-pin update fix. Users already pinned with uv tool install dyro==… should run one last uv tool install dyro==0.7.12 --force, then dyro update works. --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- src/dyro/bridge/skill/SKILL.md | 2 +- tests/test_cli.py | 4 ++-- tests/test_console_artifacts.py | 4 ++-- tests/test_release_gates.py | 2 +- uv.lock | 2 +- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b3989..5a4cf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +## 0.7.12 - 2026-08-23 + +- Fix `dyro update` for uv-managed installs that were pinned with an exact + specifier. `uv tool install dyro==0.7.11 --force` writes + `specifier = "==0.7.11"` into the tool receipt, so + `uv tool upgrade … dyro==0.7.12` is unsatisfiable. The uv-tool plan now + runs `uv tool install --force --default-index https://pypi.org/simple + --no-config dyro==` so the pin is replaced. Official PyPI and + `--no-config` stay required; pipx, pip, and uv-pip paths are unchanged. + If you already installed with `uv tool install dyro==…`, run one last + `uv tool install dyro==0.7.12 --force`. After that, `dyro update` works. + ## 0.7.11 - 2026-08-23 - Resolve `--workspace` aliases case-insensitively when exactly one diff --git a/pyproject.toml b/pyproject.toml index b41f083..e86e58d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dyro" -version = "0.7.11" +version = "0.7.12" description = "DyroEngineeringFlow: local-first automation and delivery control for multi-repository teams" readme = "README.md" requires-python = ">=3.11" diff --git a/src/dyro/bridge/skill/SKILL.md b/src/dyro/bridge/skill/SKILL.md index 1849ffe..0f0ce45 100644 --- a/src/dyro/bridge/skill/SKILL.md +++ b/src/dyro/bridge/skill/SKILL.md @@ -50,7 +50,7 @@ release, publish, console, install, or any confirmation/approval field. ```json { "protocol": {"major": 1, "minor": 0}, - "client": {"name": "dyro-agent-bridge-skill", "version": "0.7.11"}, + "client": {"name": "dyro-agent-bridge-skill", "version": "0.7.12"}, "operation": "bridge.capabilities.compact", "input": {} } diff --git a/tests/test_cli.py b/tests/test_cli.py index 8f98b39..aeb6734 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2256,6 +2256,6 @@ def test_package_version_matches_pyproject(self) -> None: from dyro import __version__ metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) - self.assertEqual(metadata["project"]["version"], "0.7.11") - self.assertEqual(__version__, "0.7.11") + self.assertEqual(metadata["project"]["version"], "0.7.12") + self.assertEqual(__version__, "0.7.12") self.assertEqual(__version__, metadata["project"]["version"]) diff --git a/tests/test_console_artifacts.py b/tests/test_console_artifacts.py index 1a488d3..c345860 100644 --- a/tests/test_console_artifacts.py +++ b/tests/test_console_artifacts.py @@ -225,12 +225,12 @@ def test_page_and_manifest_keep_p3_fail_closed_pins(self) -> None: self.assertNotEqual(refresh_at, -1) self.assertNotIn(b"/artifacts", script.body[refresh_at:next_fn]) - def test_package_version_is_0_7_11(self) -> None: + def test_package_version_is_0_7_12(self) -> None: import tomllib from pathlib import Path metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) - self.assertEqual(metadata["project"]["version"], "0.7.11") + self.assertEqual(metadata["project"]["version"], "0.7.12") class ConsoleArtifactServiceTests(WorkspaceCase): diff --git a/tests/test_release_gates.py b/tests/test_release_gates.py index 7b887fb..1b35fd3 100644 --- a/tests/test_release_gates.py +++ b/tests/test_release_gates.py @@ -32,7 +32,7 @@ def test_physics_train_refuses_published_0_6_9_tag(self) -> None: def test_0_7_release_runs_gates_without_claiming_1_0(self) -> None: stdout = StringIO() with redirect_stdout(stdout): - code = main(["--root", str(ROOT), "--release-tag", "v0.7.11"]) + code = main(["--root", str(ROOT), "--release-tag", "v0.7.12"]) self.assertEqual(code, 0) self.assertIn("0.7 gates present", stdout.getvalue()) self.assertNotIn("1.0 gates present", stdout.getvalue()) diff --git a/uv.lock b/uv.lock index 5e4aa09..bfd3e6e 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ wheels = [ [[package]] name = "dyro" -version = "0.7.11" +version = "0.7.12" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 117e9634968220302a67e248cb946b4c304cda2b Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:01:42 +0000 Subject: [PATCH 3/7] =?UTF-8?q?feat(=E5=BC=80=E5=8F=91=E7=BA=BF):=20apply?= =?UTF-8?q?=20line-family=20spawn/merge/sync=20after=20preflight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user asked to spawn, merge, or sync, /dyro-line-family must run the matching --yes command after preflight. Stay preflight-only for a dry-run ask. Never invent --push. --- CHANGELOG.md | 10 ++++ .../assets/dyro-line-family/SKILL.md | 54 +++++++++++++------ .../dyro-line-family/agents/openai.yaml | 4 +- src/dyro/integrations/manager.py | 2 +- tests/test_integrations.py | 18 +++++-- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4cf9a..cb39500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ `--no-config` stay required; pipx, pip, and uv-pip paths are unchanged. If you already installed with `uv tool install dyro==…`, run one last `uv tool install dyro==0.7.12 --force`. After that, `dyro update` works. +- `/dyro-line-family` still preflights with unchanged doctor blocking-FAIL + rules (`status` dirty=0, `line list`, matching `--dry-run`). When the + user asked to spawn / merge / sync (slash or harness; the mutation is + the ask) and preflight passes, the agent must run the matching + `line spawn|merge|sync --yes` in the same turn. It still never invents + `--push` and must not git merge / switch by hand. Preflight-only + remains when the user asked only for preflight or dry-run, or ran + `dyro --dry-run line …` themselves. CLI `--dry-run` is unchanged. On + apply success, report what ran. On preflight fail, `User action` is + still only `doctor` or the failed dry-run. ## 0.7.11 - 2026-08-23 diff --git a/src/dyro/integrations/assets/dyro-line-family/SKILL.md b/src/dyro/integrations/assets/dyro-line-family/SKILL.md index 438be83..04ee986 100644 --- a/src/dyro/integrations/assets/dyro-line-family/SKILL.md +++ b/src/dyro/integrations/assets/dyro-line-family/SKILL.md @@ -3,17 +3,20 @@ name: dyro-line-family description: > Preflight Dyro line-family ops: spawn a child line, merge a child into its direct parent, or sync the parent into the child. Use when the user - runs /dyro-line-family. Never execute the mutation. + runs /dyro-line-family. After preflight passes, apply the matching + `--yes` command in the same turn when the user asked to spawn, merge, + or sync. Stay preflight-only when they asked only for preflight or + dry-run. disable-model-invocation: true user-invocable: true argument-hint: "[workspace-alias] spawn | merge --into | sync " metadata: - short-description: "预检子线派生 / 合入父线 / 从父线同步;不要执行" + short-description: "预检后、在用户请求突变时执行子线派生 / 合入父线 / 从父线同步" --- # Dyro 开发线家族 -只做预检。这不是第一方自动座位,也不是 `task merge`。 +先预检,再按用户是否请求突变决定是否执行。这不是第一方自动座位,也不是 `task merge`。 `line spawn` / `line merge` / `line sync` 只处理 **一层父线**: - `spawn`:从已有父线派生子开发线(不是任务) @@ -30,14 +33,19 @@ If the user means **done task branch → owning line**, stop and point at `/dyro Do not run any of: -- `dyro line spawn` / `line merge` / `line sync` with `--yes` - `git merge` / `git switch` / `git checkout` - `task merge`, `task signoff`, `task gates`, `task review`, `task run` - `objective apply`, `dispatch`, `console`, push, publish - `line create` / hotfix or Change Set creation - `line post` / `line inbox` / `line ack` -Do not invent `--yes` or `--push`. Do not add `--push`. +Do not invent `--push`. Do not add `--push`. +Forbid `--yes` only when the user did not ask for the mutation, asked +only for preflight or dry-run, or they themselves ran +`dyro --dry-run line …`. CLI `--dry-run` is unchanged. +After successful preflight for an explicit spawn / merge / sync ask +(the mutation is the ask), running the matching +`line spawn|merge|sync --yes` in the same turn is required, not optional. This slash does not send family signals and must not call `line post`, `inbox`, or `ack`. Default is no push; `policy.allow_push` is not permission to invent it. Do not restore a drifted line branch. Do not add `--include-paths`. @@ -72,10 +80,27 @@ dyro --workspace --dry-run line merge --into dyro --workspace --dry-run line sync ``` -Run only the `--dry-run` that matches the requested verb. If the user supplied `--repos` on `spawn`, repeat that same `--repos` on the dry-run. -Never pass `--yes` to `line spawn`, `line merge`, or `line sync`. +Run only the `--dry-run` that matches the requested verb. If the user supplied `--repos` on `spawn`, repeat that same `--repos` on the dry-run and, when applying, on the live command. Prefer JSON when the command accepts `--format json`. One JSON document only; `kind=error` is blocked evidence. +## Apply + +When this slash or harness turn is an explicit spawn / merge / sync ask +and every preflight step passed, run exactly one matching command in +the same turn: + +```bash +dyro --workspace line spawn --yes +dyro --workspace line merge --into --yes +dyro --workspace line sync --yes +``` + +Print only the matching verb. Repeat user-supplied `--repos` on `spawn` if present. +Do not add `--push`. Do not run git merge / switch / checkout by hand. + +Stay preflight-only when the user asked only for preflight or dry-run, +or when they ran `dyro --dry-run line …` themselves. + ## Preflight 1. `doctor` is observational. Do not treat every `FAIL` as a stop. @@ -99,13 +124,10 @@ dyro --workspace doctor or the `--dry-run line spawn|merge|sync` that failed. Do not print a live command. -If every preflight step passed, say clearly that `next.commands` did not emit this, then show **one** command for the user to run personally: +If the user asked only for preflight or dry-run and every preflight step +passed, say clearly that `next.commands` did not emit this, then show +**one** command for the user to run personally. Do not run it. -```bash -dyro --workspace line spawn --yes -dyro --workspace line merge --into --yes -dyro --workspace line sync --yes -``` - -Print only the matching verb. Repeat user-supplied `--repos` on `spawn` if present. -Do not add `--push`. Do not run that command. +If the user asked to spawn / merge / sync and every preflight step +passed, run the matching `--yes` command in this turn, then report what +ran. Do not add `--push`. diff --git a/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml b/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml index d15c5be..08c8374 100644 --- a/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml +++ b/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Dyro 开发线家族" - short_description: "预检子线派生 / 合入父线 / 从父线同步;不要执行" - default_prompt: "Use $dyro-line-family. Preflight with doctor/status/line list and --dry-run line spawn|merge|sync only. Never run --yes, invent --push, or git merge. A printed command is not next.commands." + short_description: "预检后、在用户请求突变时执行子线派生 / 合入父线 / 从父线同步" + default_prompt: "Use $dyro-line-family. Preflight with doctor/status/line list and matching --dry-run. If the user asked to spawn|merge|sync and preflight passed, run the matching --yes in this turn. Never invent --push or git merge. Stay preflight-only for a dry-run/preflight ask." diff --git a/src/dyro/integrations/manager.py b/src/dyro/integrations/manager.py index 2839d2e..d31fa0b 100644 --- a/src/dyro/integrations/manager.py +++ b/src/dyro/integrations/manager.py @@ -55,7 +55,7 @@ TASK_MERGE_ASSET_VERSION = 1 LINE_FAMILY_INTEGRATION_ID = "line-family" LINE_FAMILY_SKILL_NAME = "dyro-line-family" -LINE_FAMILY_ASSET_VERSION = 1 +LINE_FAMILY_ASSET_VERSION = 2 MANIFEST_SCHEMA_VERSION = 2 LEGACY_MANIFEST_SCHEMA_VERSION = 1 _SHA256_PREFIX = "sha256:" diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 7b41e3e..62eecc4 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -225,7 +225,7 @@ def test_packaged_dispatch_skill_is_concise_and_has_required_metadata( if ": " in line: self.assertTrue(line.split(": ", 1)[1].startswith('"')) - def test_packaged_line_family_skill_is_preflight_only(self) -> None: + def test_packaged_line_family_skill_applies_when_user_asked(self) -> None: root = manager._asset_root("line-family") skill = root / "SKILL.md" metadata = root / "agents" / "openai.yaml" @@ -236,8 +236,12 @@ def test_packaged_line_family_skill_is_preflight_only(self) -> None: self.assertIn("name: dyro-line-family", content) self.assertIn("disable-model-invocation: true", content) self.assertIn("user-invocable: true", content) - self.assertIn("Never pass `--yes`", content) - self.assertIn("Do not invent `--yes` or `--push`", content) + self.assertNotIn("不要执行", content) + self.assertNotIn("Never execute the mutation", content) + self.assertIn("required, not optional", content) + self.assertIn("same turn", content) + self.assertIn("Stay preflight-only", content) + self.assertIn("Do not invent `--push`", content) self.assertIn("Do not add `--push`", content) self.assertIn("line spawn --yes", content) self.assertIn("line merge --into --yes", content) @@ -245,6 +249,7 @@ def test_packaged_line_family_skill_is_preflight_only(self) -> None: self.assertIn("--dry-run line spawn", content) self.assertIn("--dry-run line merge", content) self.assertIn("--dry-run line sync", content) + self.assertIn("report what", content) self.assertIn("git `main`", content) self.assertIn("/dyro-task-merge", content) self.assertNotIn("Any `FAIL` → stop", content) @@ -259,8 +264,11 @@ def test_packaged_line_family_skill_is_preflight_only(self) -> None: self.assertIn("`line inbox`", content) self.assertIn("`line ack`", content) self.assertIn("must not call `line post`", content) - self.assertIn("$dyro-line-family", metadata.read_text(encoding="utf-8")) - for line in metadata.read_text(encoding="utf-8").splitlines(): + yaml_text = metadata.read_text(encoding="utf-8") + self.assertIn("$dyro-line-family", yaml_text) + self.assertNotIn("不要执行", yaml_text) + self.assertIn("matching --yes", yaml_text) + for line in yaml_text.splitlines(): if ": " in line: self.assertTrue(line.split(": ", 1)[1].startswith('"')) From 0b923e4941949a3212333a5636015ddfa9b3e1fa Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:13:44 +0000 Subject: [PATCH 4/7] =?UTF-8?q?fix(=E5=BC=80=E5=8F=91=E7=BA=BF):=20preflig?= =?UTF-8?q?ht=20before=20apply;=20run=20matching=20--yes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put Preflight before Apply. --yes is illegal until all four preflight steps pass. Run only the matching verb after an explicit spawn|merge|sync ask. Control-plane and executor must not block that same-turn --yes; default forbid stays when line-family is not the active ask. Co-authored-by: Dandre Yang --- .../assets/dyro-control-plane/SKILL.md | 5 ++-- .../assets/dyro-executor/SKILL.md | 10 +++++-- .../assets/dyro-line-family/SKILL.md | 29 ++++++++++--------- .../dyro-line-family/agents/openai.yaml | 2 +- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/dyro/integrations/assets/dyro-control-plane/SKILL.md b/src/dyro/integrations/assets/dyro-control-plane/SKILL.md index 0ea3684..a91a913 100644 --- a/src/dyro/integrations/assets/dyro-control-plane/SKILL.md +++ b/src/dyro/integrations/assets/dyro-control-plane/SKILL.md @@ -16,7 +16,7 @@ When the request already supplies a workspace alias, skip global discovery and u - Git state: `dyro --workspace status --format json` - Health: `dyro --workspace doctor --format json` - One safe next step: `dyro --workspace next --format json`. If `briefing` is present, that is the switch-tool opening. `briefing.command` is a read (`tick`, `attention`, `explain`, or `list`), not a mutation, and not a resume of another harness conversation. -- Lines or hotfixes: `dyro --workspace line list [--kind line|hotfix] --format json`. Observe `parent` from that JSON. Do not run `line spawn`, `line merge`, or `line sync`. +- Lines or hotfixes: `dyro --workspace line list [--kind line|hotfix] --format json`. Observe `parent` from that JSON. Do not run `line spawn`, `line merge`, or `line sync` unless `/dyro-line-family` already completed preflight this turn for an explicit spawn|merge|sync ask — then do not block the matching `line … --yes`; line-family is the writer. Default forbid when line-family is not the active ask. - Family unread: `dyro --workspace line inbox --unacked --format json`. Report Observed as returned. User action must not invent `line post`, `line ack`, or merge. - Change Sets: `dyro --workspace changeset list --format json` or `dyro --workspace changeset verify --format json` - Installed control-plane Skill health: `dyro integration status skill --format json` @@ -53,7 +53,8 @@ If the user asks for 会审, 对抗, or Go/No-Go, follow the `dyro-board` protoc ## Hard safety boundary - Do not run `console`; it opens a local server and may launch a browser. -- Do not run `dispatch`, `objective apply`, Objective lifecycle mutations, `task gates`, task execution or lifecycle commands, line/hotfix/Change Set creation, `line spawn`, `line merge`, `line sync`, `line post`, `line ack`, integration install/sync/uninstall, setup/join/bootstrap/update, `open`, or `start`. +- Do not run `dispatch`, `objective apply`, Objective lifecycle mutations, `task gates`, task execution or lifecycle commands, line/hotfix/Change Set creation, `line post`, `line ack`, integration install/sync/uninstall, setup/join/bootstrap/update, `open`, or `start`. +- Do not run `line spawn`, `line merge`, or `line sync` unless `/dyro-line-family` already completed preflight this turn for an explicit spawn|merge|sync ask. Then do not block the matching `line … --yes`; line-family is the writer. Default forbid when line-family is not the active ask. - Do not merge, push, sign off, release, publish, delete, or edit project files. - Do not edit Dyro state files or manufacture approval/confirmation fields. - Do not treat a command printed by `doctor`, `next`, a plan, or an error as permission to run it. diff --git a/src/dyro/integrations/assets/dyro-executor/SKILL.md b/src/dyro/integrations/assets/dyro-executor/SKILL.md index 68504f4..a245623 100644 --- a/src/dyro/integrations/assets/dyro-executor/SKILL.md +++ b/src/dyro/integrations/assets/dyro-executor/SKILL.md @@ -28,9 +28,13 @@ protocol. Humans run `/dyro-review-board`. supervise other agents and do not edit a sibling task. - If a Capability Card exists for your executor and lacks `execute`, stop writing. PATH discovery is not a Card. -- Do not `git merge`, `git push`, `dyro task merge`, `line spawn`, - `line merge`, `line sync`, `task signoff`, `objective apply`, - `dispatch`, `console`, or `start` another tool. +- Do not `git merge`, `git push`, `dyro task merge`, `task signoff`, + `objective apply`, `dispatch`, `console`, or `start` another tool. +- Do not `line spawn`, `line merge`, or `line sync` unless + `/dyro-line-family` already completed preflight this turn for an + explicit spawn|merge|sync ask. Then do not block the matching + `line … --yes`; line-family is the writer. Default forbid when + line-family is not the active ask. - Do not resume another harness conversation or read vendor session directories. - Do not invent mutations from `doctor`, `next`, briefing text, or this skill. diff --git a/src/dyro/integrations/assets/dyro-line-family/SKILL.md b/src/dyro/integrations/assets/dyro-line-family/SKILL.md index 04ee986..0e0ae91 100644 --- a/src/dyro/integrations/assets/dyro-line-family/SKILL.md +++ b/src/dyro/integrations/assets/dyro-line-family/SKILL.md @@ -40,7 +40,8 @@ Do not run any of: - `line post` / `line inbox` / `line ack` Do not invent `--push`. Do not add `--push`. -Forbid `--yes` only when the user did not ask for the mutation, asked +`--yes` is illegal until all four Preflight steps pass. +Also forbid `--yes` when the user did not ask for the mutation, asked only for preflight or dry-run, or they themselves ran `dyro --dry-run line …`. CLI `--dry-run` is unchanged. After successful preflight for an explicit spawn / merge / sync ask @@ -83,6 +84,19 @@ dyro --workspace --dry-run line sync Run only the `--dry-run` that matches the requested verb. If the user supplied `--repos` on `spawn`, repeat that same `--repos` on the dry-run and, when applying, on the live command. Prefer JSON when the command accepts `--format json`. One JSON document only; `kind=error` is blocked evidence. +## Preflight + +1. `doctor` is observational. Do not treat every `FAIL` as a stop. + Stop only on **blocking** FAILs: any `FAIL` that is not missing-origin + (`FAIL ...: missing origin/`). Do not weaken other FAILs: + wrong upstream, wrong branch, missing worktree, and every other FAIL + still stop. A line that only lacks `origin/` is not a stop. +2. `status`. The lines that would be written must match the registered branch and `dirty_count` must be 0. Otherwise stop. +3. `line list --format json`. Confirm the named ids exist. For `merge`, `child.parent` must equal `--into`. For `sync`, the child must have a `parent`. One-level parent only. +4. Run the matching `--dry-run line spawn|merge|sync`. This is the real gate. Non-zero exit or `kind=error` → stop and quote the CLI error. Do not proceed. + +`--yes` is illegal until all four preflight steps pass. + ## Apply When this slash or harness turn is an explicit spawn / merge / sync ask @@ -95,23 +109,12 @@ dyro --workspace line merge --into --yes dyro --workspace line sync --yes ``` -Print only the matching verb. Repeat user-supplied `--repos` on `spawn` if present. +Run only the matching verb. Repeat user-supplied `--repos` on `spawn` if present. Do not add `--push`. Do not run git merge / switch / checkout by hand. Stay preflight-only when the user asked only for preflight or dry-run, or when they ran `dyro --dry-run line …` themselves. -## Preflight - -1. `doctor` is observational. Do not treat every `FAIL` as a stop. - Stop only on **blocking** FAILs: any `FAIL` that is not missing-origin - (`FAIL ...: missing origin/`). Do not weaken other FAILs: - wrong upstream, wrong branch, missing worktree, and every other FAIL - still stop. A line that only lacks `origin/` is not a stop. -2. `status`. The lines that would be written must match the registered branch and `dirty_count` must be 0. Otherwise stop. -3. `line list --format json`. Confirm the named ids exist. For `merge`, `child.parent` must equal `--into`. For `sync`, the child must have a `parent`. One-level parent only. -4. Run the matching `--dry-run line spawn|merge|sync`. This is the real gate. Non-zero exit or `kind=error` → stop and quote the CLI error. Do not proceed. - ## Report Use Observed / Inferred / Unknown / Plan / User action. diff --git a/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml b/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml index 08c8374..e79220a 100644 --- a/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml +++ b/src/dyro/integrations/assets/dyro-line-family/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Dyro 开发线家族" short_description: "预检后、在用户请求突变时执行子线派生 / 合入父线 / 从父线同步" - default_prompt: "Use $dyro-line-family. Preflight with doctor/status/line list and matching --dry-run. If the user asked to spawn|merge|sync and preflight passed, run the matching --yes in this turn. Never invent --push or git merge. Stay preflight-only for a dry-run/preflight ask." + default_prompt: "Use $dyro-line-family. Preflight before Apply. --yes is illegal until all four preflight steps pass. If the user asked to spawn|merge|sync and preflight passed, run the matching --yes in this turn. Never invent --push or git merge. Stay preflight-only for a dry-run/preflight ask." From f5c2787be7128313b0b35aa8b8d52bbcd5811cfd Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:13:47 +0000 Subject: [PATCH 5/7] =?UTF-8?q?test(=E5=BC=80=E5=8F=91=E7=BA=BF):=20reject?= =?UTF-8?q?=20leftover=20print-only=20line-family=20phrases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assert Print only / Do not run that command / 不要执行 / Never execute the mutation are gone, Run only the matching verb is present, and Preflight appears before Apply in the packaged skill text. Co-authored-by: Dandre Yang --- tests/test_integrations.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 62eecc4..a92ad0e 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -178,6 +178,9 @@ def test_packaged_skill_is_concise_and_has_required_metadata(self) -> None: self.assertNotIn("dyro image", content) self.assertIn("skip global discovery", content) self.assertIn("Never add `--include-paths`", content) + self.assertIn("already completed preflight this turn", content) + self.assertIn("line-family is the writer", content) + self.assertIn("Default forbid when line-family is not the active ask", content) for private_pattern in ( r"/Users/[^<\s]", r"/home/[^<\s]", @@ -238,6 +241,14 @@ def test_packaged_line_family_skill_applies_when_user_asked(self) -> None: self.assertIn("user-invocable: true", content) self.assertNotIn("不要执行", content) self.assertNotIn("Never execute the mutation", content) + self.assertNotIn("Print only the matching verb", content) + self.assertNotIn("Do not run that command", content) + self.assertIn("Run only the matching verb", content) + self.assertIn("illegal until all four preflight", content) + self.assertLess( + content.index("## Preflight"), + content.index("## Apply"), + ) self.assertIn("required, not optional", content) self.assertIn("same turn", content) self.assertIn("Stay preflight-only", content) @@ -267,6 +278,11 @@ def test_packaged_line_family_skill_applies_when_user_asked(self) -> None: yaml_text = metadata.read_text(encoding="utf-8") self.assertIn("$dyro-line-family", yaml_text) self.assertNotIn("不要执行", yaml_text) + self.assertNotIn("Print only the matching verb", yaml_text) + self.assertNotIn("Do not run that command", yaml_text) + self.assertNotIn("Never execute the mutation", yaml_text) + self.assertIn("Preflight before Apply", yaml_text) + self.assertIn("illegal until all four preflight", yaml_text) self.assertIn("matching --yes", yaml_text) for line in yaml_text.splitlines(): if ": " in line: @@ -282,6 +298,9 @@ def test_packaged_line_family_skill_applies_when_user_asked(self) -> None: self.assertIn("`line spawn`", executor) self.assertIn("`line merge`", executor) self.assertIn("`line sync`", executor) + self.assertIn("already completed preflight this turn", executor) + self.assertIn("line-family is the writer", executor) + self.assertIn("Default forbid when line-family is not the active ask", executor) def test_dispatch_skill_installs_independently_from_control_plane(self) -> None: self.assertEqual( From 0e892dcbe5afe47d738c8ff8abe15c54978e8623 Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:13:47 +0000 Subject: [PATCH 6/7] =?UTF-8?q?docs(=E5=8F=91=E5=B8=83):=20planner-safe=20?= =?UTF-8?q?0.7.12=20uv=20recovery=20and=20skill=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the full build_update_plan command with official simple index and --no-config, not bare uv tool install dyro==0.7.12 --force alone. Note dyro integration sync so hosted slash mirrors pick up asset version 2. Co-authored-by: Dandre Yang --- CHANGELOG.md | 18 +++++++++++++++--- tests/test_updates.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb39500..2e1b748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,15 @@ runs `uv tool install --force --default-index https://pypi.org/simple --no-config dyro==` so the pin is replaced. Official PyPI and `--no-config` stay required; pipx, pip, and uv-pip paths are unchanged. - If you already installed with `uv tool install dyro==…`, run one last - `uv tool install dyro==0.7.12 --force`. After that, `dyro update` works. + If you already installed with `uv tool install dyro==…`, recover with + the same planner-safe command as `build_update_plan` (official simple + index plus `--no-config`), not bare + `uv tool install dyro==0.7.12 --force` alone: + `uv tool install --force --default-index https://pypi.org/simple + --no-config dyro==0.7.12`. After that, `dyro update` works. If hosted + slash mirrors still serve the old line-family text, run + `dyro integration sync` (or a skill refresh) so they pick up asset + version 2. - `/dyro-line-family` still preflights with unchanged doctor blocking-FAIL rules (`status` dirty=0, `line list`, matching `--dry-run`). When the user asked to spawn / merge / sync (slash or harness; the mutation is @@ -22,7 +29,12 @@ remains when the user asked only for preflight or dry-run, or ran `dyro --dry-run line …` themselves. CLI `--dry-run` is unchanged. On apply success, report what ran. On preflight fail, `User action` is - still only `doctor` or the failed dry-run. + still only `doctor` or the failed dry-run. Preflight appears before + Apply, and `--yes` is illegal until all four preflight steps pass. + If `/dyro-line-family` already completed that preflight this turn for + an explicit mutation ask, `dyro-control-plane` / `dyro-executor` must + not block the matching `line … --yes`; line-family is the writer. + Default forbid stays when line-family is not the active ask. ## 0.7.11 - 2026-08-23 diff --git a/tests/test_updates.py b/tests/test_updates.py index 994d3b9..999181b 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -382,6 +382,26 @@ def which(name: str) -> str | None: editable=False, ) + def test_changelog_0_7_12_recovery_matches_planner_safe_uv_tool_install( + self, + ) -> None: + root = Path(__file__).resolve().parents[1] + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + section = changelog.split("## 0.7.12", 1)[1].split("## 0.7.11", 1)[0] + normalized = " ".join(section.split()) + self.assertIn("build_update_plan", section) + self.assertIn("https://pypi.org/simple", section) + self.assertIn("--no-config", section) + self.assertIn( + "uv tool install --force --default-index https://pypi.org/simple " + "--no-config dyro==0.7.12", + normalized, + ) + self.assertIn("not bare", normalized) + self.assertIn("uv tool install dyro==0.7.12 --force", normalized) + self.assertIn("integration sync", section) + self.assertIn("asset version 2", section) + def test_refuses_to_replace_an_editable_source_checkout(self) -> None: with self.assertRaisesRegex(DyroError, "editable"): build_update_plan("0.5.6", editable=True) From 814d0454bbd4c1642cc27bf0ef48f7598ab1c60c Mon Sep 17 00:00:00 2001 From: Dandre Yang Date: Sun, 23 Aug 2026 01:14:39 +0000 Subject: [PATCH 7/7] =?UTF-8?q?test(=E5=BC=80=E5=8F=91=E7=BA=BF):=20match?= =?UTF-8?q?=20wrapped=20default-forbid=20and=20asset-version=20phrases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Executor SKILL.md and CHANGELOG wrap those sentences; assert against whitespace-normalized text. Co-authored-by: Dandre Yang --- tests/test_integrations.py | 6 +++++- tests/test_updates.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_integrations.py b/tests/test_integrations.py index a92ad0e..8e81cde 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -295,12 +295,16 @@ def test_packaged_line_family_skill_applies_when_user_asked(self) -> None: executor = (manager._asset_root("executor") / "SKILL.md").read_text( encoding="utf-8" ) + executor_plain = " ".join(executor.split()) self.assertIn("`line spawn`", executor) self.assertIn("`line merge`", executor) self.assertIn("`line sync`", executor) self.assertIn("already completed preflight this turn", executor) self.assertIn("line-family is the writer", executor) - self.assertIn("Default forbid when line-family is not the active ask", executor) + self.assertIn( + "Default forbid when line-family is not the active ask", + executor_plain, + ) def test_dispatch_skill_installs_independently_from_control_plane(self) -> None: self.assertEqual( diff --git a/tests/test_updates.py b/tests/test_updates.py index 999181b..706de0e 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -400,7 +400,7 @@ def test_changelog_0_7_12_recovery_matches_planner_safe_uv_tool_install( self.assertIn("not bare", normalized) self.assertIn("uv tool install dyro==0.7.12 --force", normalized) self.assertIn("integration sync", section) - self.assertIn("asset version 2", section) + self.assertIn("asset version 2", normalized) def test_refuses_to_replace_an_editable_source_checkout(self) -> None: with self.assertRaisesRegex(DyroError, "editable"):