Skip to content

Commit cfaab12

Browse files
authored
Merge pull request #55 from escoffier-labs/feat/import-filter-controls
Add import filter controls
2 parents c84c73f + b88c604 commit cfaab12

8 files changed

Lines changed: 561 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5252
- `brigade work import triage` to group pending imports by source and kind.
5353
- `brigade work import dismiss` to close noisy imports without promoting them.
5454
- `brigade work import promote --all` with optional `--source` and `--kind` filters for batch promotion.
55+
- `brigade work import list/triage/promote/dismiss` metadata filters for scanner-specific fields such as `handoff_issue_category`.
56+
- `brigade work import dismiss --all` for filtered bulk dismissal of pending imports.
5557
- `brigade handoff doctor` to compare pending `.claude` and `.codex` memory handoffs against gitignored local source config.
5658
- Repo installs now include `.brigade/handoff-sources.example.json` as the local handoff ingestor source-list contract.
5759
- `brigade handoff doctor` ingestor-log checks for stale latest-run logs, skipped malformed handoffs, warning summaries, and no-reply/no-update masking signals.
5860
- `brigade handoff issues` and `brigade handoff import-issues` to turn handoff ingest warnings into grouped repair guidance and local work imports.
61+
- `brigade handoff issues --category` and `brigade handoff import-issues --category` for category-limited handoff issue review/import.
5962
- `docs/import-schema.md` documenting the local import JSONL contract for scanners and wrappers.
6063
- Cybersecurity plugin roadmap covering broad agent-workspace security checks plus Brigade-specific scanner, doctor, import, and multi-harness security checks.
6164
- Built-in `security` station and `brigade security scan` for read-only agent workspace security checks.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ brigade work import memory-care
177177
brigade work import triage
178178
brigade work import promote <import-id>
179179
brigade work import promote --all --source memory-care --kind task
180+
brigade work import promote --all --source handoff-ingest --metadata handoff_issue_category=route-skip
180181
brigade work import dismiss <import-id> --reason "not actionable"
182+
brigade work import dismiss --all --source handoff-ingest --metadata handoff_issue_category=skip --reason "historical noise"
181183
brigade work run
182184
brigade work run --queue-next
183185
brigade work run "review today's changes"
@@ -281,11 +283,11 @@ Import inbox commands:
281283
- `brigade work import ingest imports.jsonl` ingests scanner output.
282284
- `brigade work import memory-care` converts `memory/cards/decay/refresh-queue.json` into imports.
283285
- `brigade work import chat-sweep` converts `.brigade/chat-memory-sweeps/latest.json` issues into imports.
284-
- `brigade work import triage` groups pending imports by source and kind.
286+
- `brigade work import triage` groups pending imports by source and kind; use `--source`, `--kind`, and repeatable `--metadata key=value` to narrow noisy queues.
285287
- `brigade work import show <import-id>` inspects one import.
286-
- `brigade work import dismiss <import-id>` removes noise.
288+
- `brigade work import dismiss <import-id>` removes one noisy item, while `dismiss --all` closes filtered batches.
287289
- `brigade work import promote <import-id>` promotes one reviewed import into the task ledger.
288-
- `brigade work import promote --all --source memory-care --kind task` batch-promotes filtered imports.
290+
- `brigade work import promote --all --source memory-care --kind task` batch-promotes filtered imports; metadata filters also work for scanner-specific fields such as `handoff_issue_category=route-skip`.
289291

290292
Imports are stored under `.brigade/work/imports/inbox.jsonl`, stay gitignored, and do not write memory directly.
291293

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Goal: make Brigade a safe target for local automations that discover useful work
2727
- Let wrappers import candidate tasks, findings, decisions, preferences, incidents, links, and commands without knowing Brigade internals.
2828
- Convert memory-care refresh queues into local task imports.
2929
- Promote selected imports into the work task ledger, with source metadata preserved.
30-
- Dismiss noisy imports so scanners can be useful without leaving permanent queue clutter.
31-
- Batch-promote reviewed imports by source and kind.
30+
- Dismiss noisy imports so scanners can be useful without leaving permanent queue clutter. Status: started with single-item dismissal and filtered `dismiss --all`.
31+
- Batch-promote reviewed imports by source and kind. Status: started with source, kind, and metadata filters across list, triage, promote, and dismiss.
3232
- Surface pending imports and grouped counts in `brigade work brief` so discovered work appears in the daily flow.
3333

3434
## Later Phase: Chat Surface Scanners

src/brigade/cli.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,13 @@ def _build_parser() -> argparse.ArgumentParser:
120120
p_handoff_issues.add_argument("--sources", type=Path, default=None, help="Override .brigade/handoff-sources.json.")
121121
p_handoff_issues.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
122122
p_handoff_issues.add_argument("--limit", type=int, default=20, help="Maximum issue rows to print.")
123+
p_handoff_issues.add_argument("--category", action="append", default=[], help="Limit to one issue category. May be repeated.")
123124
p_handoff_import_issues = handoff_sub.add_parser("import-issues", help="Import handoff ingest issues into the work inbox.")
124125
p_handoff_import_issues.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to update.")
125126
p_handoff_import_issues.add_argument("--sources", type=Path, default=None, help="Override .brigade/handoff-sources.json.")
126127
p_handoff_import_issues.add_argument("--dry-run", action="store_true", help="Report without writing imports.")
127128
p_handoff_import_issues.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
129+
p_handoff_import_issues.add_argument("--category", action="append", default=[], help="Import only one issue category. May be repeated.")
128130

129131
# work
130132
p_work = sub.add_parser("work", help="Inspect and manage a daily Brigade work session.")
@@ -199,6 +201,14 @@ def _build_parser() -> argparse.ArgumentParser:
199201
p_work_import_list.add_argument("--all", action="store_true", help="Include promoted imports.")
200202
p_work_import_list.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
201203
p_work_import_list.add_argument("--limit", type=int, default=20, help="Maximum imports to show.")
204+
p_work_import_list.add_argument("--source", default=None, help="Filter by import source.")
205+
p_work_import_list.add_argument(
206+
"--kind",
207+
choices=["task", "finding", "decision", "preference", "incident", "link", "command"],
208+
default=None,
209+
help="Filter by import kind.",
210+
)
211+
p_work_import_list.add_argument("--metadata", action="append", default=[], help="Filter by metadata key=value. May be repeated.")
202212
p_work_import_validate = import_sub.add_parser("validate", help="Validate a work import JSONL file.")
203213
p_work_import_validate.add_argument("input_path", type=Path, help="JSONL file to validate.")
204214
p_work_import_validate.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
@@ -232,6 +242,14 @@ def _build_parser() -> argparse.ArgumentParser:
232242
p_work_import_triage.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
233243
p_work_import_triage.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
234244
p_work_import_triage.add_argument("--limit", type=int, default=50, help="Maximum imports per group to show.")
245+
p_work_import_triage.add_argument("--source", default=None, help="Filter by import source.")
246+
p_work_import_triage.add_argument(
247+
"--kind",
248+
choices=["task", "finding", "decision", "preference", "incident", "link", "command"],
249+
default=None,
250+
help="Filter by import kind.",
251+
)
252+
p_work_import_triage.add_argument("--metadata", action="append", default=[], help="Filter by metadata key=value. May be repeated.")
235253
p_work_import_show = import_sub.add_parser("show", help="Show one work import.")
236254
p_work_import_show.add_argument("import_id", help="Import id or unique prefix.")
237255
p_work_import_show.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
@@ -246,9 +264,19 @@ def _build_parser() -> argparse.ArgumentParser:
246264
help="Limit --all promotion to one kind.",
247265
)
248266
p_work_import_promote.add_argument("--source", default=None, help="Limit --all promotion to one source.")
267+
p_work_import_promote.add_argument("--metadata", action="append", default=[], help="Limit --all promotion by metadata key=value. May be repeated.")
249268
p_work_import_dismiss = import_sub.add_parser("dismiss", help="Dismiss one pending work import.")
250-
p_work_import_dismiss.add_argument("import_id", help="Import id or unique prefix.")
269+
p_work_import_dismiss.add_argument("import_id", nargs="?", help="Import id or unique prefix.")
251270
p_work_import_dismiss.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to update.")
271+
p_work_import_dismiss.add_argument("--all", action="store_true", help="Dismiss all pending imports matching filters.")
272+
p_work_import_dismiss.add_argument(
273+
"--kind",
274+
choices=["task", "finding", "decision", "preference", "incident", "link", "command"],
275+
default=None,
276+
help="Limit --all dismissal to one kind.",
277+
)
278+
p_work_import_dismiss.add_argument("--source", default=None, help="Limit --all dismissal to one source.")
279+
p_work_import_dismiss.add_argument("--metadata", action="append", default=[], help="Limit --all dismissal by metadata key=value. May be repeated.")
252280
p_work_import_dismiss.add_argument("--reason", default=None, help="Optional dismiss reason.")
253281
p_work_list = work_sub.add_parser("list", help="List recent Brigade work sessions.")
254282
p_work_list.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
@@ -651,9 +679,21 @@ def main(argv=None) -> int:
651679
if args.handoff_command == "doctor":
652680
return handoff_cmd.doctor(target=args.target, sources=args.sources, json_output=args.json)
653681
if args.handoff_command == "issues":
654-
return handoff_cmd.issues(target=args.target, sources=args.sources, json_output=args.json, limit=args.limit)
682+
return handoff_cmd.issues(
683+
target=args.target,
684+
sources=args.sources,
685+
json_output=args.json,
686+
limit=args.limit,
687+
categories=args.category,
688+
)
655689
if args.handoff_command == "import-issues":
656-
return handoff_cmd.import_issues(target=args.target, sources=args.sources, dry_run=args.dry_run, json_output=args.json)
690+
return handoff_cmd.import_issues(
691+
target=args.target,
692+
sources=args.sources,
693+
dry_run=args.dry_run,
694+
json_output=args.json,
695+
categories=args.category,
696+
)
657697
parser.error(f"unknown handoff command: {args.handoff_command}")
658698
return 2
659699
if cmd == "work":
@@ -708,6 +748,9 @@ def main(argv=None) -> int:
708748
all_imports=args.all,
709749
json_output=args.json,
710750
limit=args.limit,
751+
source=args.source,
752+
kind=args.kind,
753+
metadata=args.metadata,
711754
)
712755
if args.import_command == "validate":
713756
return work_cmd.import_validate(input_path=args.input_path, json_output=args.json)
@@ -733,7 +776,14 @@ def main(argv=None) -> int:
733776
json_output=args.json,
734777
)
735778
if args.import_command == "triage":
736-
return work_cmd.import_triage(target=args.target, json_output=args.json, limit=args.limit)
779+
return work_cmd.import_triage(
780+
target=args.target,
781+
json_output=args.json,
782+
limit=args.limit,
783+
source=args.source,
784+
kind=args.kind,
785+
metadata=args.metadata,
786+
)
737787
if args.import_command == "show":
738788
return work_cmd.import_show(target=args.target, import_id=args.import_id)
739789
if args.import_command == "promote":
@@ -743,9 +793,18 @@ def main(argv=None) -> int:
743793
all_matching=args.all,
744794
kind=args.kind,
745795
source=args.source,
796+
metadata=args.metadata,
746797
)
747798
if args.import_command == "dismiss":
748-
return work_cmd.import_dismiss(target=args.target, import_id=args.import_id, reason=args.reason)
799+
return work_cmd.import_dismiss(
800+
target=args.target,
801+
import_id=args.import_id,
802+
reason=args.reason,
803+
all_matching=args.all,
804+
kind=args.kind,
805+
source=args.source,
806+
metadata=args.metadata,
807+
)
749808
parser.error(f"unknown import command: {args.import_command}")
750809
return 2
751810
if args.work_command == "list":

src/brigade/handoff_cmd.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,11 @@ def doctor_checks(target: Path, sources: Path | None = None) -> list[tuple[str,
260260
return checks
261261

262262

263-
def collect_issues(target: Path, sources: Path | None = None) -> list[HandoffIssue]:
263+
def collect_issues(
264+
target: Path,
265+
sources: Path | None = None,
266+
categories: list[str] | None = None,
267+
) -> list[HandoffIssue]:
264268
health = inspect(target, sources=sources)
265269
issues: list[HandoffIssue] = []
266270
for inbox in health.inboxes:
@@ -320,7 +324,7 @@ def collect_issues(target: Path, sources: Path | None = None) -> list[HandoffIss
320324
)
321325
if ingestor.exists and ingestor.log_path is not None:
322326
issues.extend(_parse_ingestor_log_issues(ingestor.log_path))
323-
return _dedupe_issues(issues)
327+
return _filter_issues_by_category(_dedupe_issues(issues), categories)
324328

325329

326330
def issues(
@@ -329,6 +333,7 @@ def issues(
329333
sources: Path | None = None,
330334
json_output: bool = False,
331335
limit: int = 20,
336+
categories: list[str] | None = None,
332337
) -> int:
333338
if limit < 1:
334339
print("error: --limit must be a positive integer", file=sys.stderr)
@@ -337,7 +342,7 @@ def issues(
337342
if not target.is_dir():
338343
print(f"error: --target is not a directory: {target}", file=sys.stderr)
339344
return 2
340-
found = collect_issues(target, sources=sources)
345+
found = collect_issues(target, sources=sources, categories=categories)
341346
payload = _issues_payload(target, found)
342347
if json_output:
343348
print(json.dumps(payload, indent=2, sort_keys=True))
@@ -365,12 +370,13 @@ def import_issues(
365370
sources: Path | None = None,
366371
dry_run: bool = False,
367372
json_output: bool = False,
373+
categories: list[str] | None = None,
368374
) -> int:
369375
target = target.expanduser().resolve()
370376
if not target.is_dir():
371377
print(f"error: --target is not a directory: {target}", file=sys.stderr)
372378
return 2
373-
found = collect_issues(target, sources=sources)
379+
found = collect_issues(target, sources=sources, categories=categories)
374380
records = [issue.as_import_record() for issue in found]
375381
from . import work_cmd
376382

@@ -441,6 +447,16 @@ def _dedupe_issues(issues: list[HandoffIssue]) -> list[HandoffIssue]:
441447
return deduped
442448

443449

450+
def _filter_issues_by_category(
451+
issues: list[HandoffIssue],
452+
categories: list[str] | None,
453+
) -> list[HandoffIssue]:
454+
wanted = {category for category in categories or [] if category}
455+
if not wanted:
456+
return issues
457+
return [issue for issue in issues if issue.category in wanted]
458+
459+
444460
def _make_issue(
445461
*,
446462
category: str,

0 commit comments

Comments
 (0)