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:
+56
-6
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user