Skip to content

Commit 31f1f00

Browse files
release: heavy-coder 0.3.1 - orchestration width policy and hook routing
1 parent 2c5fbcb commit 31f1f00

13 files changed

Lines changed: 158 additions & 99 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 0.3.1
4+
5+
- **Orchestration:** centralize council width policy in `width_policy.py`; align triage and `team_coordinator.py` defaults with shipped `council_width` / `default_width` **8** (Grok Heavy phrases still widen to **16**).
6+
- **Hooks:** `should_trigger_team_plan` skips read-only inspect/audit turns unless the user also asks to implement or improve; shared width parsing in `hook_lib` inline plan build.
7+
- **Bootstrap:** advisory `recommended_flow` documents widths **3|5|8|16** with default council **8**.
8+
- **Tests:** triage width expectations, `test_hook_lib.py`, width policy parsing.
9+
310
## 0.3.0
411

512
- **Coordinator skills:** add eight strategic skills for routing, dispatch discipline, pre-dispatch enrich, candidate JSON, hook phases, repair waves, context budget, and single mode: `heavy-scope-router`, `heavy-swarm-dispatch`, `heavy-pre-dispatch-enrich`, `heavy-leaf-candidate-output`, `heavy-hook-phases`, `heavy-repair-wave`, `heavy-context-budget`, `heavy-single-mode`.

RELEASE_NOTES.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
## heavy-coder 0.3.0
1+
## heavy-coder 0.3.1
22

33
### Highlights
44

5-
- **Strategic coordinator skills (8 new):** route tasks before spending swarm tokens (`heavy-scope-router`), dispatch full-width batches once (`heavy-swarm-dispatch`), enrich slim hook context (`heavy-pre-dispatch-enrich`), require candidate-result JSON (`heavy-leaf-candidate-output`), obey hook phases (`heavy-hook-phases`), narrow repair waves (`heavy-repair-wave`), token discipline (`heavy-context-budget`), explicit single mode (`heavy-single-mode`).
6-
- **`heavy-team-default` v0.6.0:** single phase table linking explore → enrich → dispatch → synthesize → ship gate.
7-
- **Profile alignment:** `.hermes.md` / `SOUL.md` updated for default council width **8** and per-turn routing.
5+
- **Orchestration alignment:** one width policy module feeds triage, `team_coordinator.py`, and hook inline plan build so default Composer swarms stay at **8** leaves unless the task explicitly requests Grok Heavy-style **16**.
6+
- **Smarter hook triggers:** pure inspect/audit/review messages no longer inject a full council plan; mixed messages (e.g. inspect **and** improve) still do.
7+
- **Docs/diagnostics:** bootstrap advisory flow mentions **8** as the default council width.
88

99
### Install / update (local tree or GitHub)
1010

@@ -16,10 +16,10 @@ heavy-coder chat
1616
Pin a tagged checkout:
1717

1818
```bash
19-
git checkout v0.3.0
19+
git checkout v0.3.1
2020
hermes profile install . --name heavy-coder --alias --force --yes
2121
```
2222

2323
Hermes install URLs do not support `@tag` suffixes; use a local checkout to pin.
2424

25-
Full changelog: [CHANGELOG.md](https://github.com/codegraphtheory/heavy-coder/blob/v0.3.0/CHANGELOG.md)
25+
Full changelog: [CHANGELOG.md](https://github.com/codegraphtheory/heavy-coder/blob/v0.3.1/CHANGELOG.md)

agent-hooks/hook_lib.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
re.IGNORECASE,
2727
)
2828

29+
READ_ONLY_TASK_RE = re.compile(
30+
r"\b(inspect|audit|explore|check out|review|opportunities|what's wrong with|explain)\b",
31+
re.IGNORECASE,
32+
)
33+
34+
IMPLEMENTATION_TASK_RE = re.compile(
35+
r"\b(implement|improvements?|fix|add|build|patch|refactor|debug|migrate|ship|update|remove)\b",
36+
re.IGNORECASE,
37+
)
38+
2939
PHASE_IDLE = "IDLE"
3040
PHASE_AWAITING_DELEGATE = "AWAITING_DELEGATE"
3141
PHASE_AWAITING_SYNTHESIS = "AWAITING_SYNTHESIS"
@@ -118,7 +128,9 @@ def should_trigger_team_plan(text: str) -> bool:
118128
return False
119129
if is_single_mode(text):
120130
return False
121-
return not TRIVIAL_RE.search(text.strip())
131+
if TRIVIAL_RE.search(text.strip()):
132+
return False
133+
return not (READ_ONLY_TASK_RE.search(text) and not IMPLEMENTATION_TASK_RE.search(text))
122134

123135

124136
def emit_json(payload: dict[str, Any]) -> None:
@@ -153,19 +165,10 @@ def run_team_plan(task: str, repo: Path) -> dict[str, Any]:
153165
mapping = load_yaml_mapping(resolve_config_path(root))
154166
block = mapping.get("heavy_coder")
155167
heavy = block if isinstance(block, dict) else {}
156-
default_raw = heavy.get("default_width", 3)
157-
default_width = default_raw if isinstance(default_raw, int) else 3
158-
allowed = (3, 5, 8, 16)
159-
raw_widths = heavy.get("candidate_widths")
160-
if isinstance(raw_widths, list):
161-
widths = []
162-
for item in raw_widths:
163-
if isinstance(item, int):
164-
widths.append(item)
165-
elif isinstance(item, str) and item.strip().isdigit():
166-
widths.append(int(item.strip()))
167-
if widths:
168-
allowed = tuple(widths)
168+
from heavy_coder.profile_config import coerce_candidate_widths, parse_default_width
169+
170+
default_width = parse_default_width(heavy)
171+
allowed = coerce_candidate_widths(heavy.get("candidate_widths"))
169172

170173
return build_council_plan(
171174
task,

distribution.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: heavy-coder
2-
version: 0.3.0
2+
version: 0.3.1
33
description: "Hermes profile with shell-hook enforced multi-candidate coding teams (Plan 1A)."
44
hermes_requires: ">=0.12.0"
55
author: "CodeGraphTheory"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ strict = true
4141
mypy_path = "src"
4242

4343
[[tool.mypy.overrides]]
44-
module = ["jsonschema", "yaml"]
44+
module = ["jsonschema", "yaml", "hook_lib"]
4545
ignore_missing_imports = true

scripts/bootstrap_heavy_team.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def main() -> int:
8282
{
8383
"status": "OK",
8484
"hook": "bootstrap_heavy_team.py",
85-
"recommended_flow": "triage -> delegate_task(width=3|5|16) -> critique -> synthesize -> verify",
85+
"recommended_flow": "triage -> delegate_task(width=3|5|8|16, default council 8) -> critique -> synthesize -> verify",
8686
"enforcement": enforcement,
8787
"heavy_council_plugin": plugin_result,
8888
"note": "Advisory only; coordinator must follow heavy-team-default skill.",

scripts/team_coordinator.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,39 @@
77
import sys
88
from pathlib import Path
99

10-
from heavy_coder.profile_config import load_profile_config
10+
from heavy_coder.profile_config import (
11+
coerce_candidate_widths,
12+
load_profile_config,
13+
parse_default_width,
14+
)
1115
from heavy_coder.team_plan import build_team_plan
1216
from heavy_coder.triage import is_single_mode
1317

1418

15-
def _coerce_widths(raw: object) -> tuple[int, ...]:
16-
if not isinstance(raw, list):
17-
return (3, 5, 16)
18-
widths: list[int] = []
19-
for item in raw:
20-
if isinstance(item, int):
21-
widths.append(item)
22-
elif isinstance(item, str) and item.strip().isdigit():
23-
widths.append(int(item.strip()))
24-
return tuple(widths) if widths else (3, 5, 16)
25-
26-
2719
def _load_team_plan_kwargs(repo: Path) -> dict[str, object]:
2820
try:
2921
profile = load_profile_config(repo)
3022
except (FileNotFoundError, RuntimeError, ValueError):
23+
from heavy_coder.width_policy import DEFAULT_CANDIDATE_WIDTHS, DEFAULT_TRIAGE_WIDTH
24+
3125
return {
3226
"heavy_council_always": False,
33-
"heavy_council_width": 16,
34-
"default_width": 3,
35-
"allowed_widths": (3, 5, 16),
27+
"heavy_council_width": 8,
28+
"default_width": DEFAULT_TRIAGE_WIDTH,
29+
"allowed_widths": DEFAULT_CANDIDATE_WIDTHS,
3630
}
3731

3832
from heavy_coder.profile_config import load_yaml_mapping, resolve_config_path
3933

4034
mapping = load_yaml_mapping(resolve_config_path(repo))
4135
heavy = mapping.get("heavy_coder")
4236
block = heavy if isinstance(heavy, dict) else {}
43-
default_raw = block.get("default_width", 3)
44-
default_width = default_raw if isinstance(default_raw, int) else 3
4537

4638
return {
4739
"heavy_council_always": profile.heavy_council_always,
4840
"heavy_council_width": profile.council_width,
49-
"default_width": default_width,
50-
"allowed_widths": _coerce_widths(block.get("candidate_widths")),
41+
"default_width": parse_default_width(block),
42+
"allowed_widths": coerce_candidate_widths(block.get("candidate_widths")),
5143
}
5244

5345

@@ -56,7 +48,7 @@ def main() -> int:
5648
parser.add_argument("task", nargs="?", help="Task description (or use --task-file)")
5749
parser.add_argument("--task-file", type=Path, help="Read task from a file")
5850
parser.add_argument("--repo", type=Path, default=Path("."), help="Target repository root")
59-
parser.add_argument("--width", type=int, help="Override triage width (3, 5, or 16 heavy council)")
51+
parser.add_argument("--width", type=int, help="Override triage width (must be in candidate_widths, e.g. 3, 5, 8, 16)")
6052
parser.add_argument(
6153
"--heavy-council",
6254
action="store_true",

src/heavy_coder/profile_config.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,31 @@
1212
yaml = None
1313

1414
from heavy_coder.council_injection import CouncilPresentation, parse_council_presentation
15+
from heavy_coder.width_policy import (
16+
DEFAULT_CANDIDATE_WIDTHS,
17+
DEFAULT_COUNCIL_WIDTH,
18+
DEFAULT_MIN_DELEGATE_TASKS,
19+
DEFAULT_TRIAGE_WIDTH,
20+
coerce_candidate_widths,
21+
parse_default_width,
22+
)
23+
24+
# Re-export for callers that import from profile_config.
25+
__all__ = [
26+
"DEFAULT_CANDIDATE_WIDTHS",
27+
"DEFAULT_COUNCIL_WIDTH",
28+
"DEFAULT_MIN_DELEGATE_TASKS",
29+
"DEFAULT_TRIAGE_WIDTH",
30+
"ProfileConfig",
31+
"coerce_candidate_widths",
32+
"load_profile_config",
33+
"load_yaml_mapping",
34+
"parse_default_width",
35+
"parse_heavy_coder_block",
36+
"resolve_config_path",
37+
]
1538

16-
DEFAULT_MIN_DELEGATE_TASKS = 8
1739
DEFAULT_HEAVY_COUNCIL_ALWAYS = False
18-
DEFAULT_COUNCIL_WIDTH = 8
1940

2041

2142
@dataclass(frozen=True)

src/heavy_coder/triage.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import re
66
from dataclasses import dataclass
77

8+
from heavy_coder.width_policy import DEFAULT_CANDIDATE_WIDTHS, DEFAULT_COUNCIL_WIDTH
9+
810
HIGH_RISK_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
911
re.compile(p, re.IGNORECASE)
1012
for p in (
@@ -63,17 +65,17 @@ def is_single_mode(text: str) -> bool:
6365
def classify_task(
6466
task: str,
6567
*,
66-
default_width: int = 3,
67-
allowed_widths: tuple[int, ...] = (3, 5, 16),
68-
heavy_council_width: int = HEAVY_COUNCIL_WIDTH,
68+
default_width: int = DEFAULT_COUNCIL_WIDTH,
69+
allowed_widths: tuple[int, ...] = DEFAULT_CANDIDATE_WIDTHS,
70+
heavy_council_width: int = DEFAULT_COUNCIL_WIDTH,
6971
heavy_council_always: bool = False,
7072
) -> TriageResult:
7173
text = task.strip()
7274
reasons: list[str] = []
7375
council_w = heavy_council_width if heavy_council_width in allowed_widths else max(allowed_widths)
7476

7577
if any(p.search(text) for p in HEAVY_COUNCIL_PATTERNS):
76-
width = council_w
78+
width = HEAVY_COUNCIL_WIDTH if HEAVY_COUNCIL_WIDTH in allowed_widths else council_w
7779
reasons.append("heavy council / Grok Heavy emulation signals in task text")
7880
elif heavy_council_always and not is_single_mode(text):
7981
width = council_w

src/heavy_coder/width_policy.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Shared council width constants and config parsing (no heavy_coder import cycles)."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
DEFAULT_MIN_DELEGATE_TASKS = 8
8+
DEFAULT_COUNCIL_WIDTH = 8
9+
DEFAULT_CANDIDATE_WIDTHS: tuple[int, ...] = (3, 5, 8, 16)
10+
DEFAULT_TRIAGE_WIDTH = 8
11+
12+
13+
def coerce_candidate_widths(raw: object) -> tuple[int, ...]:
14+
"""Parse ``heavy_coder.candidate_widths`` from config (shared by CLI and hooks)."""
15+
if not isinstance(raw, list):
16+
return DEFAULT_CANDIDATE_WIDTHS
17+
widths: list[int] = []
18+
for item in raw:
19+
if isinstance(item, int) and not isinstance(item, bool):
20+
widths.append(item)
21+
elif isinstance(item, str) and item.strip().isdigit():
22+
widths.append(int(item.strip()))
23+
return tuple(widths) if widths else DEFAULT_CANDIDATE_WIDTHS
24+
25+
26+
def parse_default_width(block: dict[str, Any] | None, *, fallback: int = DEFAULT_TRIAGE_WIDTH) -> int:
27+
heavy = block if isinstance(block, dict) else {}
28+
default_raw = heavy.get("default_width", fallback)
29+
if isinstance(default_raw, int) and not isinstance(default_raw, bool):
30+
return default_raw
31+
if isinstance(default_raw, str) and default_raw.strip().isdigit():
32+
return int(default_raw.strip())
33+
return fallback

0 commit comments

Comments
 (0)