idle-draft: per-work-type allowed_tools; research must web-search for stories

Research and topic_ideas runs get --allowedTools WebSearch,WebFetch;
research template mandates >=1 web search for related stories with
full-URL + access-date citations under the dossier's status labels.

Claude-Session: https://claude.ai/code/session_01YQDoWNM7XPPii28khFWoMc
This commit is contained in:
Paul O'Reilly
2026-08-02 21:32:35 +12:00
parent cd5be1cf92
commit 75add58246
6 changed files with 158 additions and 8 deletions

View File

@@ -88,8 +88,8 @@ cat > "$FIXTURE_CONFIG" <<EOF
"work_types": {
"review": { "providers": ["anthropic"] },
"draft": { "providers": ["anthropic"] },
"research": { "providers": ["anthropic", "minimax"] },
"topic_ideas": { "providers": ["anthropic", "minimax"] }
"research": { "providers": ["anthropic", "minimax"], "allowed_tools": ["WebSearch", "WebFetch"] },
"topic_ideas": { "providers": ["anthropic", "minimax"], "allowed_tools": ["WebSearch", "WebFetch"] }
},
"dossiers": ["ai"],
"review_score_threshold": 11,
@@ -135,6 +135,7 @@ assert_contains "$output" "item: ai/03-topic-three" "dryrun picks item 03 (
assert_contains "$output" "work_type: review" "dryrun names work_type review"
assert_contains "$output" "provider: anthropic" "dryrun names provider anthropic"
assert_not_contains "$output" "would run" "dryrun does not execute claude"
assert_not_contains "$output" "--allowedTools" "dryrun argv for review (no allowed_tools configured) omits the flag"
echo "-- dryrun: exact resolved claude argv for a research item --"
# Force only the research-ready item to be eligible by pointing --repo at a
@@ -151,6 +152,8 @@ output=$("$SCRIPT" --config "$FIXTURE_CONFIG" --repo "$SOLO" --probe-json "$FIXT
assert_contains "$output" "work_type: research" "dryrun (solo fixture) picks research"
assert_contains "$output" "argv: ['claude', '-p', '--max-turns', '25', '--add-dir'" "dryrun prints resolved claude argv with --add-dir for research"
assert_contains "$output" "$TMPDIR/evidence" "dryrun argv includes configured evidence dir"
assert_contains "$output" "--allowedTools" "dryrun argv for research includes --allowedTools (allowed_tools configured)"
assert_contains "$output" "WebSearch,WebFetch" "dryrun argv --allowedTools value is the comma-joined tool list"
echo "-- dryrun: no mutation --"
before_hash=$(cd "$FIXTURE_REPO" && git rev-parse HEAD)
@@ -182,6 +185,31 @@ output=$("$SCRIPT" --config "$BADCONFIG" --repo "$FIXTURE_REPO" --probe-json "$F
code=$?
assert_exit_code "$code" 2 "config missing required keys exits 2"
BADALLOWED="$TMPDIR/bad-allowed.config.json"
cat > "$BADALLOWED" <<EOF
{
"parallel": 2,
"providers": {
"anthropic": { "profile": "$PROFILE_ANTHROPIC", "threshold_pct": 80, "five_hour_ceiling": 50, "min_idle": 5 }
},
"work_types": {
"review": { "providers": ["anthropic"] },
"draft": { "providers": ["anthropic"] },
"research": { "providers": ["anthropic"], "allowed_tools": "WebSearch" },
"topic_ideas": { "providers": ["anthropic"] }
},
"dossiers": ["ai"],
"review_score_threshold": 11,
"max_unreviewed_research_per_dossier": 3,
"max_open_topic_proposals": 6,
"evidence_dirs": ["$TMPDIR/evidence"]
}
EOF
output=$("$SCRIPT" --config "$BADALLOWED" --repo "$FIXTURE_REPO" --probe-json "$FIXTURE_PROBE" --dryrun 2>&1)
code=$?
assert_exit_code "$code" 2 "allowed_tools not a list of strings exits 2"
assert_contains "$output" "allowed_tools" "allowed_tools config error names the offending key"
echo "-- mark subcommand round-trip --"
MARKREPO="$TMPDIR/mark-repo"
mkdir -p "$MARKREPO/ai"
@@ -398,6 +426,55 @@ check("build_child_env: profile without provider.env gets only CLAUDE_CONFIG_DIR
"ANTHROPIC_BASE_URL" not in env2 and "ANTHROPIC_API_KEY" not in env2 and env2["CLAUDE_CONFIG_DIR"] == str(no_provider_env_dir),
env2)
# --- allowed_tools / --allowedTools argv ---
argv = m.build_claude_argv("model-x", 25, ["/tmp/ev"], ["WebSearch", "WebFetch"])
check("build_claude_argv: appends --allowedTools with comma-joined list when provided",
"--allowedTools" in argv and argv[argv.index("--allowedTools") + 1] == "WebSearch,WebFetch", argv)
argv_empty = m.build_claude_argv("model-x", 25, ["/tmp/ev"], [])
check("build_claude_argv: omits --allowedTools when allowed_tools is an empty list", "--allowedTools" not in argv_empty, argv_empty)
argv_none = m.build_claude_argv("model-x", 25, ["/tmp/ev"], None)
check("build_claude_argv: omits --allowedTools when allowed_tools is None", "--allowedTools" not in argv_none, argv_none)
cfg_at = {
"work_types": {
"research": {"providers": ["anthropic"], "allowed_tools": ["WebSearch", "WebFetch"]},
"draft": {"providers": ["anthropic"]},
}
}
check("resolve_allowed_tools: returns configured list for research",
m.resolve_allowed_tools(cfg_at, "research") == ["WebSearch", "WebFetch"], m.resolve_allowed_tools(cfg_at, "research"))
check("resolve_allowed_tools: returns [] when work_type has no allowed_tools key",
m.resolve_allowed_tools(cfg_at, "draft") == [], m.resolve_allowed_tools(cfg_at, "draft"))
check("resolve_allowed_tools: returns [] for a work_type not present in config at all",
m.resolve_allowed_tools(cfg_at, "review") == [], m.resolve_allowed_tools(cfg_at, "review"))
bad_allowed_cfg_path = tmpdir / "bad-allowed-direct.config.json"
bad_allowed_cfg = json.loads((tmpdir / "idle-draft.config.json").read_text())
bad_allowed_cfg["work_types"]["research"]["allowed_tools"] = "WebSearch"
bad_allowed_cfg_path.write_text(json.dumps(bad_allowed_cfg))
try:
m.load_config(bad_allowed_cfg_path)
check("load_config: allowed_tools as a bare string (not a list) raises ConfigError", False)
except m.ConfigError as e:
check("load_config: allowed_tools as a bare string (not a list) raises ConfigError", "allowed_tools" in str(e), str(e))
bad_allowed_cfg2_path = tmpdir / "bad-allowed-direct2.config.json"
bad_allowed_cfg2 = json.loads((tmpdir / "idle-draft.config.json").read_text())
bad_allowed_cfg2["work_types"]["research"]["allowed_tools"] = ["WebSearch", 5]
bad_allowed_cfg2_path.write_text(json.dumps(bad_allowed_cfg2))
try:
m.load_config(bad_allowed_cfg2_path)
check("load_config: allowed_tools list containing a non-string raises ConfigError", False)
except m.ConfigError as e:
check("load_config: allowed_tools list containing a non-string raises ConfigError", "allowed_tools" in str(e), str(e))
good_allowed_cfg = m.load_config(tmpdir / "idle-draft.config.json")
check("load_config: well-formed allowed_tools (list of strings) loads without error",
good_allowed_cfg["work_types"]["research"]["allowed_tools"] == ["WebSearch", "WebFetch"],
good_allowed_cfg["work_types"]["research"])
# --- Citation validation ---
real_path = cred_repo / "exists.txt"
real_path.write_text("x")