fix: per-entity Step 0.55, LAW 7 sub-run quiet, default 2, canonical SKILL.md (#311)

Four fixes based on 2026-04-22 test-window feedback on v3.0.11 --competitors:

- Each competitor sub-run now runs Step 0.55 (X handle / subreddits /
  GitHub) via resolve.auto_resolve inside the fanout closure. Deep-copied
  config per entity prevents _auto_resolve_context leak across sub-runs.
  Resolved data stored on report.artifacts["resolved"] for the renderer.
- New internal_subrun keyword on planner.plan_query and pipeline.run
  suppresses the LAW 7 "No --plan passed" stderr for engine-internal
  fan-out only. Default path unchanged.
- Default --competitors count is now 2 (3-way total). --competitors=N
  still customizes; range 1..6.
- SKILL.md STEP 0 canonical-path self-check forces readers who loaded
  from marketplaces/ (auto-restored to origin/main, stale) to re-read
  from plugins/cache/last30days-skill/last30days/{VERSION}/SKILL.md.
  Two of three 2026-04-22 test windows hit this stale-path trap.
- New ## Resolved Entities block in render_comparison_multi shows
  per-entity handles/subs/github for debug visibility.

Bumps plugin.json to 3.0.12. 12 new tests; 1,175 total passing.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-04-22 21:30:08 -07:00
committed by GitHub
parent 5f054380c5
commit 00d01933e0
13 changed files with 1019 additions and 23 deletions
+56 -6
View File
@@ -230,11 +230,11 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument(
"--competitors",
nargs="?",
const=3,
const=2,
type=int,
default=None,
metavar="N",
help="Auto-discover N competitor entities and fan out last30days across all of them as a comparison (default N=3, range 1..6). Use --competitors-list to override discovery.",
help="Auto-discover N competitor entities and fan out last30days across all of them as a comparison (default N=2 → 3-way: original + 2 peers; range 1..6). Use --competitors-list to override discovery.",
)
parser.add_argument(
"--competitors-list",
@@ -246,7 +246,7 @@ def build_parser() -> argparse.ArgumentParser:
COMPETITORS_MIN = 1
COMPETITORS_MAX = 6
COMPETITORS_DEFAULT = 3
COMPETITORS_DEFAULT = 2
def resolve_competitors_args(args: argparse.Namespace) -> tuple[bool, int, list[str]]:
@@ -463,7 +463,7 @@ def main() -> int:
comp_enabled, comp_count, comp_explicit = resolve_competitors_args(args)
def _main_runner() -> schema.Report:
return pipeline.run(
r = pipeline.run(
topic=topic,
config=config,
depth=depth,
@@ -481,6 +481,15 @@ def main() -> int:
github_user=github_user,
github_repos=github_repos,
)
r.artifacts["resolved"] = {
"entity": topic,
"x_handle": (args.x_handle or "").lstrip("@"),
"subreddits": list(subreddits or []),
"github_user": (github_user or ""),
"github_repos": list(github_repos or []),
"context": config.get("_auto_resolve_context", "") or "",
}
return r
if comp_enabled:
from lib import competitors as competitors_mod
@@ -517,15 +526,56 @@ def main() -> int:
)
def _competitor_runner(entity: str) -> schema.Report:
return pipeline.run(
# Deep-copy config so per-entity auto_resolve context does not
# leak across sub-runs. Each sub-run writes its own
# `_auto_resolve_context` into its local config copy.
entity_config = dict(config)
resolved = {
"entity": entity,
"x_handle": "",
"subreddits": [],
"github_user": "",
"github_repos": [],
"context": "",
}
if not args.mock and resolve_mod._has_backend(entity_config):
try:
r = resolve_mod.auto_resolve(entity, entity_config)
except Exception as exc:
sys.stderr.write(
f"[Competitors] auto_resolve failed for {entity!r}: "
f"{type(exc).__name__}: {exc}\n"
)
r = {}
resolved["x_handle"] = r.get("x_handle", "") or ""
resolved["subreddits"] = list(r.get("subreddits") or [])
resolved["github_user"] = r.get("github_user", "") or ""
resolved["github_repos"] = list(r.get("github_repos") or [])
resolved["context"] = r.get("context", "") or ""
if resolved["context"]:
entity_config["_auto_resolve_context"] = resolved["context"]
sys.stderr.write(
f"[Competitors] {entity}: "
f"x=@{resolved['x_handle'] or '-'} "
f"subs={len(resolved['subreddits'])} "
f"gh={resolved['github_user'] or '-'}\n"
)
report = pipeline.run(
topic=entity,
config=config,
config=entity_config,
depth=depth,
requested_sources=requested_sources,
mock=args.mock,
x_handle=resolved["x_handle"] or None,
subreddits=resolved["subreddits"] or None,
github_user=resolved["github_user"] or None,
github_repos=resolved["github_repos"] or None,
web_backend=args.web_backend,
lookback_days=args.lookback_days,
internal_subrun=True,
)
report.artifacts["resolved"] = resolved
return report
entity_reports = fanout.run_competitor_fanout(
main_topic=topic,
+2
View File
@@ -178,6 +178,7 @@ def run(
lookback_days: int = 30,
github_user: str | None = None,
github_repos: list[str] | None = None,
internal_subrun: bool = False,
) -> schema.Report:
settings = DEPTH_SETTINGS[depth]
requested_sources = normalize_requested_sources(requested_sources)
@@ -215,6 +216,7 @@ def run(
provider=None if mock else reasoning_provider,
model=None if mock else runtime.planner_model,
context=config.get("_auto_resolve_context", ""),
internal_subrun=internal_subrun,
)
# Source labelling: the fallback path annotates notes with "fallback-plan"
# or "deterministic-comparison-plan"; anything else came from the LLM.
+19 -11
View File
@@ -86,9 +86,16 @@ def plan_query(
provider: providers.ReasoningClient | None,
model: str | None,
context: str = "",
internal_subrun: bool = False,
) -> schema.QueryPlan:
"""Create a query plan. Comparison queries with extractable entities use a
deterministic plan; other intents prefer the configured reasoning provider."""
deterministic plan; other intents prefer the configured reasoning provider.
internal_subrun: when True, suppress the LAW 7 "No --plan passed" stderr
warning. LAW 7 targets the hosting-reasoning-model path; competitor
fan-out sub-runs are engine-internal and the warning is a false positive
there. Default False preserves the warning on every user-facing invocation.
"""
if _should_force_deterministic_plan(topic):
return _fallback_plan(
topic,
@@ -122,16 +129,17 @@ def plan_query(
# planner credentials - NOT a prerequisite the caller needs. If you are
# the hosting reasoning model, YOU are the provider. LAW 7 / 2026-04-19
# Hermes Agent Use Cases failure mode.
import sys
print(
"[Planner] No --plan passed. If you are the reasoning model hosting "
"this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime), "
"YOU ARE the planner: generate a JSON query plan yourself and pass it "
"via --plan. You do not need an API key or credentials; you ARE the "
"LLM. The deterministic fallback below is the headless/cron path only. "
"See LAW 7 in SKILL.md and Step 0.75 for the plan schema.",
file=sys.stderr,
)
if not internal_subrun:
import sys
print(
"[Planner] No --plan passed. If you are the reasoning model hosting "
"this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime), "
"YOU ARE the planner: generate a JSON query plan yourself and pass it "
"via --plan. You do not need an API key or credentials; you ARE the "
"LLM. The deterministic fallback below is the headless/cron path only. "
"See LAW 7 in SKILL.md and Step 0.75 for the plan schema.",
file=sys.stderr,
)
return _fallback_plan(topic, available_sources, requested_sources, depth)
+55
View File
@@ -445,6 +445,11 @@ def render_comparison_multi(
)
lines.append("")
resolved_block = _render_resolved_entities_block(entity_reports)
if resolved_block:
lines.extend(resolved_block)
lines.append("")
fun_params = _FUN_LEVELS.get(fun_level, _FUN_LEVELS["medium"])
for label, report in entity_reports:
lines.extend(_render_entity_evidence_block(
@@ -475,6 +480,52 @@ def render_comparison_multi(
return "\n".join(lines).strip() + "\n"
def _render_resolved_entities_block(
entity_reports: list[tuple[str, schema.Report]],
) -> list[str]:
"""Emit a visible per-entity Step 0.55 resolution summary.
Reads `resolved` dicts from each Report's artifacts. Returns an empty
list when no entity has a resolved payload (mock mode, no web backend,
or artifacts not populated). Missing per-entity fields render as `-`.
Context strings truncate at 120 chars.
"""
any_resolved = any(
isinstance(report.artifacts.get("resolved"), dict)
for _label, report in entity_reports
)
if not any_resolved:
return []
out: list[str] = ["## Resolved Entities", ""]
for label, report in entity_reports:
resolved = report.artifacts.get("resolved") or {}
x_handle = resolved.get("x_handle") or ""
subs = resolved.get("subreddits") or []
gh_user = resolved.get("github_user") or ""
gh_repos = resolved.get("github_repos") or []
context = resolved.get("context") or ""
x_display = f"@{x_handle}" if x_handle else "-"
subs_display = (
", ".join(f"r/{s}" for s in subs[:5]) + (
f" (+{len(subs) - 5})" if len(subs) > 5 else ""
)
) if subs else "-"
gh_display = f"@{gh_user}" if gh_user else "-"
if gh_repos:
gh_display += f" ({', '.join(gh_repos[:3])}" + (
f" +{len(gh_repos) - 3}" if len(gh_repos) > 3 else ""
) + ")"
context_display = _truncate(context, 120) if context else "-"
out.append(
f"- **{label}**: X {x_display} | Subs {subs_display} | "
f"GitHub {gh_display} | Context: {context_display}"
)
return out
def _render_entity_evidence_block(
*,
label: str,
@@ -536,6 +587,10 @@ def render_comparison_multi_context(
_AI_SAFETY_NOTE,
"",
]
resolved_block = _render_resolved_entities_block(entity_reports)
if resolved_block:
lines.extend(resolved_block)
lines.append("")
for label, report in entity_reports:
lines.append(f"## {label}")
lines.append(f"Intent: {report.query_plan.intent}")