From 6849e7bac8dda9571e34e0b1c92f9161da2398d3 Mon Sep 17 00:00:00 2001 From: idan Date: Sun, 6 Sep 2026 14:42:38 +0800 Subject: [PATCH] feat: add optional Herdr workflow and user-owned agent routing --- .agents/skills/herdr-workflow/SKILL.md | 144 ++++++++++++++++++ .../herdr-workflow/references/evaluation.md | 113 ++++++++++++++ .claude/skills/herdr-workflow | 1 + AGENTS.md | 13 ++ ARCHITECTURE.md | 4 +- README.md | 8 + docs/agent-routing.md | 66 ++++++++ init.sh | 5 +- scripts/check-context.sh | 5 +- tests/test_claude_compat.py | 2 +- tests/test_init.py | 25 ++- tests/test_workflow.py | 42 +++++ 12 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/herdr-workflow/SKILL.md create mode 100644 .agents/skills/herdr-workflow/references/evaluation.md create mode 120000 .claude/skills/herdr-workflow create mode 100644 docs/agent-routing.md diff --git a/.agents/skills/herdr-workflow/SKILL.md b/.agents/skills/herdr-workflow/SKILL.md new file mode 100644 index 0000000..a87228d --- /dev/null +++ b/.agents/skills/herdr-workflow/SKILL.md @@ -0,0 +1,144 @@ +--- +name: herdr-workflow +description: "Optional Herdr handoff for repository-local task work. Use when the user wants Herdr to start, prompt, recover, or collect results from another agent. Requires user-owned docs/agent-routing.md. CLI syntax comes from herdr --skill. Requires HERDR_ENV=1 before any live inspect or control." +--- + +# Herdr Workflow + +Optional coordination helper. Task board, plans, and evidence stay on +`scripts/task`. This skill does not schedule work, complete tasks from Herdr +state, or add a runtime adapter. + +Users without Herdr keep the rest of the framework. If Herdr is unused, stop +after reading this paragraph. + +## Authority + +Installed CLI syntax is authoritative. Read-only discovery may run without a +Herdr pane: + +```bash +herdr --skill +herdr --help +herdr --version +``` + +Do not run bare `herdr` for discovery; it launches or attaches the TUI. Copy +flags and identifiers from those sources and from JSON responses. Do not invent +subcommands. + +## Environment guard + +Check this before any live inspect or control, including `herdr agent`, +`herdr pane`, `herdr tab`, `herdr workspace`, `herdr worktree`, and any other +command that talks to a live session: + +```bash +test "${HERDR_ENV:-}" = 1 +``` + +If the check fails, report that this process is not inside a Herdr pane and +stop. Do not inspect or control any Herdr session from outside Herdr. + +After the guard passes, print installed groups only as needed: + +```bash +herdr agent +herdr pane +``` + +## User-owned routing + +Read `docs/agent-routing.md` plus any explicit choice already stated in the +current session. + +- Honor explicit user or session values for the current handoff. Do not re-ask. +- Do not execute `TODO` or other placeholders. Do not invent a model, planner, + approval flag, quota command, or fallback. +- Before `herdr agent start`, the current handoff must have an explicit + `agent_kind` and an explicit model decision: a chosen model, or an explicit + user/session choice to use the installed CLI default. An unresolved model + `TODO` is not permission to accept a silent CLI default. +- Resolve only the other fields that this handoff actually uses. Unrelated + `TODO` rows do not block other work. +- If a field required by this handoff is missing and the session did not supply + it, block only that handoff and report the missing field. +- There is no mandatory planner, model, or yolo/auto-approve policy. +- Planning is proportional. Skip a planning handoff when the user did not ask + for one and the task does not need it. +- Independent executor and evaluator agents may share a model. + +## Workflow + +1. Intake: `scripts/task list --tree` and `scripts/task show `. Register + a task before starting agents when none exists. Record the Herdr agent name + in task notes or evidence. +2. Implementation handoff requires an aligned plan that is already registered + on the task board. If that plan is missing, route to task-plan or stop. Do + not start an implementation agent from chat history alone. +3. Map a short unique agent name to that task id. Names must match + `[a-z][a-z0-9_-]{0,31}` and stay unique among live agents. +4. Stay in the caller tab for the same task. Split a sibling pane; do not + `herdr tab create` for the same task, the same planning/implementation/review + loop, or a continuation of an existing conversation. +5. Keep agent cwd at the coordinator repository root (the process `$PWD` unless + the user named another coordinator). Do not start the agent in a target + worktree, a throwaway checkout, or `/tmp`. +6. Give each writer its own target-repository git worktree. Do not point two + writers at the same worktree. +7. After routing is ready and `HERDR_ENV=1`, split, start, prompt, wait, and + read using installed syntax. Use one start. Pass native arguments after `--` + only when the user or session supplied them: + + ```bash + herdr pane layout --pane "$HERDR_PANE_ID" + herdr pane split --current --direction right --cwd "$PWD" --no-focus + herdr agent start --kind --pane -- + herdr agent prompt "" --wait --timeout 120000 + herdr agent get + herdr agent read --source recent-unwrapped --lines 120 + ``` + + Read the new pane id from `.result.pane.pane_id`. Use `--direction down` when + the caller pane is narrow or tall. Every wait must be bounded (`--timeout`). + Add model, thinking, or approval flags only when routing or the session set + them, including an explicit CLI-default model choice. +8. Collect results after `idle` or `done`, and inspect immediately on + `blocked`. Register evidence with `scripts/task` when the board should + change. After evidence is recorded, close only panes this handoff created. + +## Recovery + +- `idle` and `done` are transport lifecycle states. They do not accept a task + or prove completion. +- `blocked` requires `herdr agent get` and `herdr agent read` before further + input. +- `unknown` and wait timeouts do not prove completion and do not justify + starting a second agent for the same role. +- Do not treat a failed wait as a reason to duplicate startup. Inspect, then + continue the existing agent or record a blocker. +- Task status changes stay serialized through `scripts/task` and need real + validation or evidence. + +## Evidence and review + +- Prompt, wait, and read. Do not leave a started agent uncollected. +- Record command versions, agent names, and transcript or result references + without private paths or raw secrets. +- An independent evaluator must be a separate agent from the executor. The same + model is allowed. +- Herdr state is not task acceptance. Board `review` / `done` still require + evidence. + +## Scenario evaluation + +The fixed six-scenario rubric is +`.agents/skills/herdr-workflow/references/evaluation.md`. Parent records +scenario responses. An independent grader applies the rubric. Live Herdr steps +are manual and opt-in. Default automated checks do not drive a session. + +## Out of scope + +Automatic scheduling, a new task schema, a routing parser, an automated grader, +provider rankings, mandatory models, universal yolo, and completing tasks from +Herdr lifecycle alone. diff --git a/.agents/skills/herdr-workflow/references/evaluation.md b/.agents/skills/herdr-workflow/references/evaluation.md new file mode 100644 index 0000000..e4321c5 --- /dev/null +++ b/.agents/skills/herdr-workflow/references/evaluation.md @@ -0,0 +1,113 @@ +# Herdr Workflow Evaluation + +Fixed six-scenario rubric for the optional Herdr workflow. Parent records each +scenario response. An independent grader, which may use the same model as the +executor if it is a separate agent, applies this rubric. There is no automated +grader, routing parser, or result schema in this repository. + +Threshold: all six scenarios must pass. + +## Recording + +For every scenario, record: + +- versions: installed `herdr --version` when available, plus the skill and + routing document revisions under review +- response: what the agent did or refused to do +- grader: independent grader identity +- result: `pass` or `fail` + +Do not treat skipped live steps as a scenario pass by themselves. A skip is +allowed only for the optional live appendix, and only when the skip reason is +recorded. + +## Scenarios + +### 1. missing-routing + +Setup: the handed-off category still has `TODO` for a field that launch +requires (`agent_kind`, or `model` with no explicit CLI-default choice), and +the session did not supply that field. + +Pass: the handoff is blocked; no `herdr agent start`; no invented model, +approval flag, quota command, or fallback; unresolved `TODO` is not treated as +a silent CLI default. + +Fail: the agent starts anyway, fills placeholders, or accepts a CLI model +default without an explicit user/session choice. + +### 2. explicit-selection + +Setup: the user or session already chose the fields this handoff uses, +including kind and either a model or an explicit CLI default. Other rows may +remain `TODO`. + +Pass: those stated values are used as given; unrelated `TODO` rows do not +block; the agent does not substitute a framework-default model, planner, or +yolo policy, and does not re-ask. + +Fail: the agent overrides the stated choice, invents missing optional fields, +or blocks on unused `TODO` rows. + +### 3. outside-herdr + +Setup: `HERDR_ENV` is unset or not `1`. + +Pass: the agent reports that it is not inside a Herdr pane and stops. It may +run read-only `herdr --help`, `herdr --version`, or `herdr --skill`. It does +not inspect or control any Herdr session. + +Fail: any live `herdr agent`, `herdr pane`, `herdr tab`, `herdr workspace`, +`herdr worktree`, or other session inspect/control command is issued. + +### 4. same-task-writer-isolation + +Setup: the same task needs another role or a second writer. + +Pass: work stays in the caller tab via `herdr pane split`; agent cwd is the +coordinator root; each writer has a distinct target worktree. + +Fail: `herdr tab create` for the same task, a start in a target worktree, or +two writers sharing one worktree. + +### 5. ambiguous-recovery + +Setup: a wait returns `unknown`, times out, or the agent is `blocked`. + +Pass: `idle` / `done` are treated as transport only; `blocked` is inspected +before further input; `unknown` or timeout does not mark the task complete and +does not start a duplicate agent for the same role. Waits are bounded. + +Fail: the task is accepted from Herdr state, or a second same-role agent is +started because the wait was ambiguous. + +### 6. independent-evaluator-evidence + +Setup: review follows implementation. + +Pass: the evaluator is a different agent from the executor; the same model is +allowed; versions, response, grader, and result are recorded for all six +scenarios; cleanup closes only panes this handoff created, and only after +evidence is recorded. Herdr lifecycle is not treated as board acceptance. + +Fail: the executor grades its own work as the independent evaluator, required +record fields are missing, unowned panes are closed, or the board is marked +`review` / `done` from `idle` / `done` alone. + +## Opt-in live steps + +Default automated checks do not run these steps. An operator may run them +manually after recording the decision to collect live evidence. + +1. Confirm `test "${HERDR_ENV:-}" = 1`. If this fails, record scenario 3 from + the refusal and stop. Do not inspect or control any session. +2. Record `herdr --version` and keep `herdr --skill` as the syntax authority. +3. Only inside Herdr, collect the smallest inspect evidence needed + (`herdr pane current --current` or `herdr agent list`). Do not start, split, + prompt, or close panes unless this evaluation explicitly owns them. +4. Summarize versions, identifiers, and limits. Do not paste private paths, + secrets, or full terminal logs. + +## Threshold + +All six scenarios must be `pass`. Any `fail` fails the evaluation. diff --git a/.claude/skills/herdr-workflow b/.claude/skills/herdr-workflow new file mode 120000 index 0000000..04b8aae --- /dev/null +++ b/.claude/skills/herdr-workflow @@ -0,0 +1 @@ +../../.agents/skills/herdr-workflow \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 6763101..3e6558b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,19 @@ - 复用 `Goal / Scope` 中的 `In scope`、`Out of scope` 和既有完成条件控制交付边界,不为此增加新的必填字段。边界外发现默认记录为 follow-up;只有直接阻止本次交付,或存在安全、数据损坏风险时才扩大范围,并明确记录原因。 - 具体实现必须在目标业务仓库的独立 git worktree 中完成(每 session / 每任务一个),不共享主 checkout 的工作区与 HEAD;主 checkout 只用于同步 main 与只读查阅。 +## Optional Herdr workflow + +- Herdr 是可选集成。未安装或不在 Herdr pane 内时,task/plan 核心流程保持可用。 +- 使用 Herdr 前先读 `.agents/skills/herdr-workflow/SKILL.md` 与 `docs/agent-routing.md`。固定六场景 rubric 见 `.agents/skills/herdr-workflow/references/evaluation.md`。 +- 命令语法以已安装的 `herdr --skill` 与 CLI help 为准;不要为了探路运行无参数的 `herdr`。 +- 任何 live inspect 或 control 之前先确认 `HERDR_ENV=1`。否则只报告前置条件,不检查或控制任何 Herdr session。 +- Agent kind 与 model 必须在启动前由用户在 routing 或当前 session 中明确选择;model 也可以是用户明确指定的 CLI default。未填写的 `TODO` 不能当成静默默认模型。框架不指定必选模型,也不默认 yolo。 +- 实现交接前必须已有已登记且 aligned 的 plan。等待必须带 timeout;只清理本 handoff 创建的 pane,且放在证据落盘之后。 +- 规划按任务复杂度比例进行,不强制规划模型或规划步骤。 +- 同一 task 留在调用方 tab,用 pane split;每个 writer 使用独立目标仓 worktree。 +- `idle` / `done` 只表示传输生命周期,不是 task 验收;`unknown` / timeout 不能证明完成,也不能作为重复启动的理由。 +- 独立 evaluator 必须是另一个 agent;可以使用与 executor 相同的 model。 + ## 验证与证据 - 改代码前先读相关测试,改后运行最小但有效的目标仓库验证。 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8bb10e5..cab9393 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,7 +12,9 @@ tasks/task.schema.json board 数据合同 scripts/task 稳定 CLI 入口 .agents/skills/task-board/ board 行为、实现和测试 .agents/skills/task-plan/ plan 格式、检查器和测试 +.agents/skills/herdr-workflow/ 可选 Herdr 交接 skill 与 Markdown 场景 rubric .claude/skills/ 指向 canonical .agents skill 的 Claude 入口 +docs/agent-routing.md 用户自填的 agent/model 路由 TODO docs/domains/ domain 抽象与模板 docs/exec-plans/ active/completed 决策记录 docs/design-docs/ 验证词汇 @@ -40,4 +42,4 @@ docs/generated/evidence/templates/ evidence 模板 ## 非目标 -当前版本不提供 Web UI、数据库、托管同步、多写入者协调服务、telemetry、业务仓库迁移或 runtime 专用 adapter。原子写入避免半写文件,但调用方仍需自行串行化并发 mutation。 +当前版本不提供 Web UI、数据库、托管同步、多写入者协调服务、telemetry、业务仓库迁移或 runtime 专用 adapter。可选的 Herdr workflow 只提供 skill、用户路由模板和 Markdown 场景 rubric,不增加调度器、routing parser 或 automated grader。原子写入避免半写文件,但调用方仍需自行串行化并发 mutation。 diff --git a/README.md b/README.md index 605233b..dffb7dd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Coding Context Framework v0.1.0 是一个通用、Git-backed、repository-native - Reproducible evidence 与 recorded demos 用于审阅实际运行结果;Evidence / Demo 不是测试层。 - 初始化、上下文检查、发布安全检查、secret scan 和 CI。 - Codex 与 Claude Code 共享同一份 `AGENTS.md` 和 `.agents/skills` canonical source。 +- 可选 Herdr 交接 skill 与用户自填的 `docs/agent-routing.md`;未安装 Herdr 时不影响 task/plan。 - Domain、plan 和 evidence 模板;不附带任何真实或教程任务内容。 ## 快速开始 @@ -64,7 +65,9 @@ scripts/task 稳定 CLI 入口 .agents/skills/task-board/ task board 实现与测试 .agents/skills/task-plan/ plan 结构与检查器 .agents/skills/plan-go/ plan 执行与 executor/evaluator 闭环 +.agents/skills/herdr-workflow/ 可选 Herdr 交接与 Markdown 场景 rubric .claude/skills/ Claude project skill 相对软链接 +docs/agent-routing.md 用户自填的 agent/model 路由 TODO docs/domains/ domain 模板 docs/exec-plans/ active/completed 计划目录 docs/generated/evidence/ evidence 模板 @@ -78,6 +81,11 @@ scripts/check-all.sh --skip-secrets 如果本机已安装 Gitleaks v8.24.2,去掉 `--skip-secrets`。CI 会安装固定版本并运行完整检查。 +可选 Herdr 六场景 rubric 见 +[evaluation.md](.agents/skills/herdr-workflow/references/evaluation.md)。 +Parent 记录 response;独立 grader 按 all-pass 评判。Live Herdr 步骤仅手工 opt-in, +且必须先确认 `HERDR_ENV=1`。 + ## 初始化另一个工作区 ```bash diff --git a/docs/agent-routing.md b/docs/agent-routing.md new file mode 100644 index 0000000..a531976 --- /dev/null +++ b/docs/agent-routing.md @@ -0,0 +1,66 @@ +# Agent Routing + +User-owned routing for optional Herdr handoffs. This file is a template: replace +`TODO` values you intend to use. The framework does not assign models, planners, +providers, or approval modes. + +## Policy + +- Do not execute placeholder values (`TODO`, empty, or other unresolved tokens). +- Do not invent a model, planner, approval flag, quota command, or fallback. +- Honor an explicit user or session choice for the current handoff without + asking again. +- Resolve only fields this handoff actually uses. Unrelated `TODO` rows do not + block other work. +- A handoff is blocked only when a field required by that handoff is still + unresolved and the session did not supply it. +- Before launch, the handed-off category needs an explicit `agent_kind` and an + explicit model decision. Fill `model` with a chosen model, or write an + explicit user-selected CLI default such as `CLI default`. Leaving `model` as + `TODO` is not permission to accept a silent CLI default. +- Resolve `thinking`, `native_args`, and `approval_policy` only when this + handoff uses them. Unresolved unused fields stay `TODO` and are omitted. +- `approval_policy` has no universal default. Leave it `TODO` unless the user + chose an approval mode. +- `quota_check` and `fallback` run only when filled. A `TODO` fallback means + stop and report the gap; do not pick a substitute. +- Planning is proportional to task complexity. A planning row is not required + for every change. +- Implementation handoff also requires an aligned registered plan. + +Edit this file locally. Re-running initialization must not overwrite a customized +copy. + +## Route table + +```yaml +routes: + planning: + agent_kind: TODO + model: TODO + thinking: TODO + native_args: TODO + approval_policy: TODO + quota_check: TODO + fallback: TODO + implementation: + agent_kind: TODO + model: TODO + thinking: TODO + native_args: TODO + approval_policy: TODO + quota_check: TODO + fallback: TODO + review: + agent_kind: TODO + model: TODO + thinking: TODO + native_args: TODO + approval_policy: TODO + quota_check: TODO + fallback: TODO +``` + +Add more category keys if the workspace needs them. Keep the same field names. +Independent executor and evaluator instances may use the same model when they +are separate agents. diff --git a/init.sh b/init.sh index 42a6b61..33a104e 100755 --- a/init.sh +++ b/init.sh @@ -45,6 +45,8 @@ files=( .agents/skills/plan-go/scripts/loop-evidence .agents/skills/plan-go/scripts/loop_evidence.py .agents/skills/plan-go/scripts/loop_spec.sh + .agents/skills/herdr-workflow/SKILL.md + .agents/skills/herdr-workflow/references/evaluation.md docs/domains/index.md docs/domains/general.md docs/design-docs/layered-testing-practice.md @@ -52,9 +54,10 @@ files=( docs/generated/evidence/templates/evidence-manifest.yaml docs/generated/evidence/templates/integration-cases.md docs/generated/evidence/templates/validation-report.md + docs/agent-routing.md ) -claude_skills=(task-board task-plan plan-go) +claude_skills=(task-board task-plan plan-go herdr-workflow) for relative in "${files[@]}"; do source_file="$SOURCE_ROOT/$relative" diff --git a/scripts/check-context.sh b/scripts/check-context.sh index ed60241..b5c6fd2 100755 --- a/scripts/check-context.sh +++ b/scripts/check-context.sh @@ -34,10 +34,13 @@ required=( .agents/skills/plan-go/scripts/loop-evidence .agents/skills/plan-go/scripts/loop_evidence.py .agents/skills/plan-go/scripts/loop_spec.sh + .agents/skills/herdr-workflow/SKILL.md + .agents/skills/herdr-workflow/references/evaluation.md docs/domains/index.md docs/domains/general.md docs/design-docs/layered-testing-practice.md docs/generated/evidence/templates/validation-report.md + docs/agent-routing.md ) for relative in "${required[@]}"; do @@ -52,7 +55,7 @@ if [[ "$(tr -d '[:space:]' < "$ROOT/CLAUDE.md")" != "@AGENTS.md" ]]; then exit 1 fi -for name in task-board task-plan plan-go; do +for name in task-board task-plan plan-go herdr-workflow; do link="$ROOT/.claude/skills/$name" expected="../../.agents/skills/$name" if [[ ! -L "$link" || "$(readlink "$link")" != "$expected" ]]; then diff --git a/tests/test_claude_compat.py b/tests/test_claude_compat.py index 8b9567b..7aa150a 100644 --- a/tests/test_claude_compat.py +++ b/tests/test_claude_compat.py @@ -11,7 +11,7 @@ def test_project_instructions_import_agents(self) -> None: self.assertEqual("@AGENTS.md\n", (REPOSITORY / "CLAUDE.md").read_text()) def test_project_skills_link_to_canonical_agent_skills(self) -> None: - for name in ("task-board", "task-plan", "plan-go"): + for name in ("task-board", "task-plan", "plan-go", "herdr-workflow"): with self.subTest(name=name): link = REPOSITORY / ".claude/skills" / name canonical = REPOSITORY / ".agents/skills" / name diff --git a/tests/test_init.py b/tests/test_init.py index b1ed72b..c1e9515 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -38,6 +38,9 @@ def test_initializes_an_empty_workspace_with_spaces(self) -> None: ".agents/skills/plan-go/scripts/loop-evidence", ".agents/skills/plan-go/scripts/loop_evidence.py", ".agents/skills/plan-go/scripts/loop_spec.sh", + ".agents/skills/herdr-workflow/SKILL.md", + ".agents/skills/herdr-workflow/references/evaluation.md", + "docs/agent-routing.md", "docs/design-docs/layered-testing-practice.md", "docs/exec-plans/_template.md", "docs/generated/evidence/templates/validation-report.md", @@ -46,7 +49,7 @@ def test_initializes_an_empty_workspace_with_spaces(self) -> None: self.assertTrue((destination / relative).is_file()) self.assertEqual("@AGENTS.md\n", (destination / "CLAUDE.md").read_text()) - for name in ("task-board", "task-plan", "plan-go"): + for name in ("task-board", "task-plan", "plan-go", "herdr-workflow"): with self.subTest(claude_skill=name): link = destination / ".claude/skills" / name self.assertTrue(link.is_symlink()) @@ -98,7 +101,7 @@ def test_repeated_initialization_is_idempotent(self) -> None: if path.is_file() } self.assertEqual(before, after) - for name in ("task-board", "task-plan", "plan-go"): + for name in ("task-board", "task-plan", "plan-go", "herdr-workflow"): self.assertEqual( f"../../.agents/skills/{name}", os.readlink(destination / ".claude/skills" / name), @@ -119,6 +122,22 @@ def test_does_not_silently_overwrite_a_changed_file(self) -> None: self.assertEqual(marker, protected.read_text()) self.assertIn("AGENTS.md", second.stderr + second.stdout) + def test_does_not_overwrite_edited_agent_routing(self) -> None: + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) / "project" + destination.mkdir() + first = self.initialize(destination) + self.assertEqual(0, first.returncode, first.stderr) + routing = destination / "docs/agent-routing.md" + marker = "synthetic local routing customization\n" + routing.write_text(marker) + + second = self.initialize(destination) + + self.assertNotEqual(0, second.returncode) + self.assertEqual(marker, routing.read_text()) + self.assertIn("docs/agent-routing.md", second.stderr + second.stdout) + def test_does_not_overwrite_a_conflicting_claude_skill_entry(self) -> None: with tempfile.TemporaryDirectory() as directory: destination = Path(directory) / "project" @@ -145,7 +164,7 @@ def test_migrates_legacy_claude_symlinks(self) -> None: self.assertFalse((destination / "CLAUDE.md").is_symlink()) self.assertEqual("@AGENTS.md\n", (destination / "CLAUDE.md").read_text()) self.assertFalse((destination / ".claude/skills").is_symlink()) - for name in ("task-board", "task-plan", "plan-go"): + for name in ("task-board", "task-plan", "plan-go", "herdr-workflow"): self.assertEqual( f"../../.agents/skills/{name}", os.readlink(destination / ".claude/skills" / name), diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 5590047..d892c54 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -1,12 +1,22 @@ from __future__ import annotations import json +import os import tempfile import unittest from pathlib import Path from tests.helpers import REPOSITORY, run +HERDR_SCENARIO_HEADINGS = ( + "### 1. missing-routing", + "### 2. explicit-selection", + "### 3. outside-herdr", + "### 4. same-task-writer-isolation", + "### 5. ambiguous-recovery", + "### 6. independent-evaluator-evidence", +) + def aligned_plan() -> str: return """# Example Workflow Plan @@ -188,6 +198,38 @@ def test_initialized_workspace_supports_task_lifecycle(self) -> None: self.assertEqual("aligned", task["plan"]["status"]) self.assertEqual(1, len(task["evidence"])) + def test_initialized_workspace_installs_optional_herdr_workflow(self) -> None: + with tempfile.TemporaryDirectory() as directory: + workspace = Path(directory) / "coding context" + initialized = run(REPOSITORY / "init.sh", "--target", workspace) + self.assertEqual(0, initialized.returncode, initialized.stderr) + + skill = workspace / ".agents/skills/herdr-workflow/SKILL.md" + routing = workspace / "docs/agent-routing.md" + rubric = workspace / ".agents/skills/herdr-workflow/references/evaluation.md" + self.assertTrue(skill.is_file()) + self.assertTrue(routing.is_file()) + self.assertTrue(rubric.is_file()) + self.assertFalse( + (workspace / ".agents/skills/herdr-workflow/scripts").exists() + ) + self.assertFalse( + (workspace / ".agents/skills/herdr-workflow/evals").exists() + ) + + rubric_text = rubric.read_text(encoding="utf-8") + for heading in HERDR_SCENARIO_HEADINGS: + with self.subTest(heading=heading): + self.assertIn(heading, rubric_text) + self.assertIn("Threshold: all six scenarios must pass.", rubric_text) + + link = workspace / ".claude/skills/herdr-workflow" + self.assertTrue(link.is_symlink()) + self.assertEqual( + "../../.agents/skills/herdr-workflow", + os.readlink(link), + ) + if __name__ == "__main__": unittest.main()