Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
show/watch/resume surfaces). Recoverable from git history; see #442 / #471.

### Added
- `brigade work resolve-target --cwd PATH [--harness NAME]` prints the
Brigade-wired project root (requires `.brigade/config.json`) so shell hooks
share Claude's discovery contract instead of matching any `.brigade/` dir.
- Shared `brigade.wiring.resolve_wired_target` helper used by Claude hooks and
the new resolver CLI; optional harness filter.
- Grok work-loop hook templates under `src/brigade/templates/grok/hooks/` that
use `resolve-target`, timeout the session brief, and do not deny edits while
a brief is running (#536).
- Imported `stations/notify` Go module into the Brigade monorepo. Unified release
manifests now enumerate five native components (25 platform assets plus
`component-manifest-v1.json` and `checksums.txt`) with managed resolution
Expand All @@ -31,6 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `brigade mcp sync --user-scope` (and `brigade operator sync-mcp --user-scope`) no longer writes stdio MCP servers into a user-wide client config silently: interactive runs show the destination, stdio count, and the servers-times-sessions process formula and ask for confirmation, non-interactive and `--json` runs require `--allow-global-stdio`, and plan/sync items now carry `transport` and `scope`. (#349)

### Fixed
- Grok/T3 work-loop discovery no longer treats `~/.brigade` (user-level aboyeur
roster) as a project work root. Hooks and `work resolve-target` require
`.brigade/config.json`, so sessions under `$HOME` or unwired dirs do not
background `brigade work brief --target $HOME` (#536).
- `brigade run` no longer dies on the first unparsable plan when the chef's final
message is prose. The corrective plan turn now restates the output contract
("reply with the JSON plan object and nothing else") alongside the parse error,
Expand Down
3 changes: 2 additions & 1 deletion docs/command-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ enabled: run `brigade extras on` once, or set `BRIGADE_EXTRAS=1`.
- `brigade untrusted` (extras): 2 command path(s)
- `brigade update`: 1 command path(s)
- `brigade version`: 1 command path(s)
- `brigade work`: 144 command path(s)
- `brigade work`: 145 command path(s)
- `brigade workflow` (extras): 3 command path(s)

## Commands
Expand Down Expand Up @@ -666,6 +666,7 @@ enabled: run `brigade extras on` once, or set `BRIGADE_EXTRAS=1`.
- `brigade work plan-proposals`
- `brigade work plans`
- `brigade work recap`
- `brigade work resolve-target`
- `brigade work resume`
- `brigade work review closeout`
- `brigade work review finding-show`
Expand Down
24 changes: 1 addition & 23 deletions src/brigade/claude_hooks/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import Any, Iterator

from .. import localio
from ..config import load_config
from ..wiring import resolve_wired_target
from .package import PACKAGE_REF

BRIEF_TIMEOUT_SECONDS = 10
Expand Down Expand Up @@ -116,28 +116,6 @@ def iter_session_states(
yield state


def resolve_wired_target(cwd: object) -> Path | None:
if not isinstance(cwd, str) or not cwd.strip():
return None
try:
current = Path(cwd).expanduser().resolve()
except OSError:
return None
if not current.is_dir():
current = current.parent
for candidate in (current, *current.parents):
if not (candidate / ".brigade" / "config.json").is_file():
continue
try:
config = load_config(candidate)
except (OSError, ValueError, json.JSONDecodeError):
return None
if config is not None and "claude" in config.selection.harnesses:
return candidate
return None
return None


def _advance_quote_state(text: str, quote: str | None) -> str | None:
index = 0
while index < len(text):
Expand Down
16 changes: 16 additions & 0 deletions src/brigade/cli/work/dispatching.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ def dispatch(args) -> int:
return work_cmd.resume(target=args.target)
if args.work_command == "brief":
return work_cmd.brief(target=args.target, limit=args.limit, json_output=args.json)
if args.work_command == "resolve-target":
from ...wiring import resolve_wired_target

harness = args.harness.strip() if isinstance(args.harness, str) and args.harness.strip() else None
try:
cwd = str(args.cwd.expanduser().resolve())
except OSError:
cwd = str(args.cwd)
target = resolve_wired_target(cwd, harness=harness)
if args.json:
print(json.dumps({"ok": target is not None, "target": str(target) if target else None}, indent=2))
return 0 if target is not None else 1
if target is None:
return 1
print(target)
return 0
if args.work_command == "hooks":
from ...claude_hooks import install_cmd

Expand Down
16 changes: 16 additions & 0 deletions src/brigade/cli/work/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ def register(sub: argparse._SubParsersAction) -> None:
p_work_brief.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_work_brief.add_argument("--limit", type=int, default=3, help="Maximum recent sessions to include.")
p_work_brief.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
p_work_resolve_target = work_sub.add_parser(
"resolve-target",
help="Print the Brigade-wired project root for a cwd (requires .brigade/config.json).",
)
p_work_resolve_target.add_argument(
"--cwd",
type=Path,
default=Path("."),
help="Starting directory to walk upward from.",
)
p_work_resolve_target.add_argument(
"--harness",
default=None,
help="Require this harness in .brigade/config.json (default: any wired project).",
)
p_work_resolve_target.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
p_work_hooks = work_sub.add_parser("hooks", help="Manage project-scoped Claude work-loop hooks.")
hooks_sub = p_work_hooks.add_subparsers(dest="hooks_command", metavar="<hooks-command>")
hooks_sub.required = True
Expand Down
61 changes: 61 additions & 0 deletions src/brigade/templates/grok/hooks/brigade-work.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "REPLACE_WITH_HOOK_SCRIPT",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash|run_terminal_command|Edit|Write|MultiEdit|search_replace|write_file|apply_patch",
"hooks": [
{
"type": "command",
"command": "REPLACE_WITH_HOOK_SCRIPT",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash|run_terminal_command|Edit|Write|MultiEdit|search_replace|write_file|apply_patch",
"hooks": [
{
"type": "command",
"command": "REPLACE_WITH_HOOK_SCRIPT",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "REPLACE_WITH_HOOK_SCRIPT",
"timeout": 30
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "REPLACE_WITH_HOOK_SCRIPT",
"timeout": 30
}
]
}
]
}
}
Loading
Loading