devcouncil 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import asyncio
|
|
5
|
-
import time
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
from rich.panel import Panel
|
|
11
|
-
from rich.table import Table
|
|
12
|
-
|
|
13
|
-
from devcouncil.live.cards import (
|
|
14
|
-
card_path,
|
|
15
|
-
filter_cards,
|
|
16
|
-
get_card,
|
|
17
|
-
load_cards,
|
|
18
|
-
review_turn,
|
|
19
|
-
save_card,
|
|
20
|
-
update_card_status,
|
|
21
|
-
)
|
|
22
|
-
from devcouncil.live.repair_prompt import build_bulk_live_repair_prompt, build_live_repair_prompt
|
|
23
|
-
from devcouncil.live.reviewer import LiveReviewService
|
|
24
|
-
from devcouncil.live.signals import ReviewSignal, load_signals, mark_processed
|
|
25
|
-
from devcouncil.live.summary import live_review_summary
|
|
26
|
-
from devcouncil.live.tasks import active_task_id
|
|
27
|
-
from devcouncil.live.transcripts import discover_sessions, latest_assistant_turn, load_turns
|
|
28
|
-
from devcouncil.app.config import get_api_key, load_config
|
|
29
|
-
from devcouncil.llm.provider import create_provider, validate_model_provider
|
|
30
|
-
from devcouncil.llm.router import ModelRouter
|
|
31
|
-
from devcouncil.telemetry.traces import TraceLogger
|
|
32
|
-
|
|
33
|
-
app = typer.Typer(help="Review active coding-agent sessions and emit critique cards.")
|
|
34
|
-
console = Console()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@app.command("sessions")
|
|
38
|
-
def sessions(
|
|
39
|
-
client: str = typer.Option("claude", "--client", help="Agent client: claude or generic."),
|
|
40
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
41
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
42
|
-
):
|
|
43
|
-
"""List coding-agent transcripts DevCouncil can review."""
|
|
44
|
-
root = project_root.expanduser().resolve()
|
|
45
|
-
found = discover_sessions(root, client=client)
|
|
46
|
-
if json_format:
|
|
47
|
-
typer.echo(json.dumps({"sessions": [item.model_dump() for item in found]}, indent=2))
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
table = Table(title="DevCouncil Watch Sessions")
|
|
51
|
-
table.add_column("Client", style="cyan")
|
|
52
|
-
table.add_column("Session")
|
|
53
|
-
table.add_column("Turns", justify="right")
|
|
54
|
-
table.add_column("Transcript")
|
|
55
|
-
for item in found:
|
|
56
|
-
table.add_row(item.client, item.id, str(item.turns), item.transcript_path)
|
|
57
|
-
console.print(table)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@app.command("review")
|
|
61
|
-
def review(
|
|
62
|
-
transcript: Path | None = typer.Option(None, "--transcript", "-t", help="JSONL transcript to review."),
|
|
63
|
-
session: str | None = typer.Option(None, "--session", help="Discovered session ID to review, or 'latest'."),
|
|
64
|
-
latest: bool = typer.Option(False, "--latest", help="Review the most recently updated discovered session."),
|
|
65
|
-
client: str = typer.Option("generic", "--client", help="Agent client: claude, codex, gemini, cursor, or generic."),
|
|
66
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
67
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
68
|
-
persist: bool = typer.Option(True, "--persist/--no-persist", help="Save the critique card under .devcouncil/live/cards."),
|
|
69
|
-
llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
|
|
70
|
-
force: bool = typer.Option(False, "--force", help="Regenerate and overwrite an existing card for this turn."),
|
|
71
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Associate the critique card with a DevCouncil task."),
|
|
72
|
-
):
|
|
73
|
-
"""Review the latest assistant response in a coding-agent transcript."""
|
|
74
|
-
root = project_root.expanduser().resolve()
|
|
75
|
-
transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
|
|
76
|
-
if transcript_path is None:
|
|
77
|
-
message = "No transcript selected. Use --transcript, --session, or --latest."
|
|
78
|
-
if json_format:
|
|
79
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
80
|
-
else:
|
|
81
|
-
console.print(f"[red]{message}[/red]")
|
|
82
|
-
raise typer.Exit(code=2)
|
|
83
|
-
turn = latest_assistant_turn(transcript_path, client=client)
|
|
84
|
-
if turn is None:
|
|
85
|
-
message = f"No assistant turn found in {transcript_path}."
|
|
86
|
-
if json_format:
|
|
87
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
88
|
-
else:
|
|
89
|
-
console.print(f"[yellow]{message}[/yellow]")
|
|
90
|
-
raise typer.Exit(code=1)
|
|
91
|
-
|
|
92
|
-
scoped_task_id = task_id or active_task_id(root)
|
|
93
|
-
card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
|
|
94
|
-
saved_path, duplicate = _save_card_once(root, card, persist=persist, force=force)
|
|
95
|
-
if saved_path:
|
|
96
|
-
_log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="review")
|
|
97
|
-
payload = card.model_dump()
|
|
98
|
-
if saved_path:
|
|
99
|
-
payload["path"] = str(saved_path)
|
|
100
|
-
payload["duplicate"] = duplicate
|
|
101
|
-
|
|
102
|
-
if json_format:
|
|
103
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
_print_card(card)
|
|
107
|
-
if saved_path:
|
|
108
|
-
console.print(f"[green]Saved critique card:[/green] {saved_path}")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@app.command("cards")
|
|
112
|
-
def cards(
|
|
113
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
114
|
-
limit: int = typer.Option(10, "--limit", "-n", help="Maximum cards to show."),
|
|
115
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Filter cards by task ID."),
|
|
116
|
-
status: str | None = typer.Option(None, "--status", help="Filter by status: open, resolved, or ignored."),
|
|
117
|
-
verdict: str | None = typer.Option(None, "--verdict", help="Filter by verdict: approved, concerns, or critical."),
|
|
118
|
-
client: str | None = typer.Option(None, "--client", help="Filter by coding-agent client."),
|
|
119
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
120
|
-
):
|
|
121
|
-
"""Show previously emitted critique cards."""
|
|
122
|
-
root = project_root.expanduser().resolve()
|
|
123
|
-
if limit < 1:
|
|
124
|
-
message = "--limit must be greater than 0."
|
|
125
|
-
if json_format:
|
|
126
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
127
|
-
else:
|
|
128
|
-
console.print(f"[red]{message}[/red]")
|
|
129
|
-
raise typer.Exit(code=2)
|
|
130
|
-
found, error, _argument = filter_cards(
|
|
131
|
-
load_cards(root),
|
|
132
|
-
task_id=task_id,
|
|
133
|
-
status=status,
|
|
134
|
-
verdict=verdict,
|
|
135
|
-
client=client,
|
|
136
|
-
)
|
|
137
|
-
if error:
|
|
138
|
-
if json_format:
|
|
139
|
-
typer.echo(json.dumps({"ok": False, "error": error}, indent=2))
|
|
140
|
-
else:
|
|
141
|
-
console.print(f"[red]{error}[/red]")
|
|
142
|
-
raise typer.Exit(code=2)
|
|
143
|
-
total = len(found)
|
|
144
|
-
found = found[:limit]
|
|
145
|
-
if json_format:
|
|
146
|
-
typer.echo(json.dumps({
|
|
147
|
-
"cards": [item.model_dump() for item in found],
|
|
148
|
-
"filters": {
|
|
149
|
-
"task_id": task_id,
|
|
150
|
-
"status": status,
|
|
151
|
-
"verdict": verdict,
|
|
152
|
-
"client": client,
|
|
153
|
-
},
|
|
154
|
-
"limit": limit,
|
|
155
|
-
"total": total,
|
|
156
|
-
}, indent=2))
|
|
157
|
-
return
|
|
158
|
-
for card in found:
|
|
159
|
-
_print_card(card)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@app.command("status")
|
|
163
|
-
def status(
|
|
164
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
165
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Task scope for blocking-card calculation."),
|
|
166
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
167
|
-
):
|
|
168
|
-
"""Show compact live-review status, blockers, pending signals, and recent cards."""
|
|
169
|
-
root = project_root.expanduser().resolve()
|
|
170
|
-
payload = live_review_summary(root, task_id=task_id)
|
|
171
|
-
if json_format:
|
|
172
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
173
|
-
return
|
|
174
|
-
blockers = payload["blocking_cards"]
|
|
175
|
-
|
|
176
|
-
summary = Table(title="DevCouncil Live Review Status")
|
|
177
|
-
summary.add_column("Metric", style="cyan")
|
|
178
|
-
summary.add_column("Value")
|
|
179
|
-
summary.add_row("Active task", payload["active_task_id"] or "(none)")
|
|
180
|
-
summary.add_row("Scope task", payload["scope_task_id"] or "(unscoped)")
|
|
181
|
-
summary.add_row("Pending signals", str(payload["pending_signals"]))
|
|
182
|
-
summary.add_row("Cards", str(payload["cards"]["total"]))
|
|
183
|
-
summary.add_row("Open cards", str(payload["cards"]["open"]))
|
|
184
|
-
summary.add_row("Open critical cards", str(payload["cards"]["critical_open"]))
|
|
185
|
-
summary.add_row("Blocking cards in scope", str(len(blockers)))
|
|
186
|
-
console.print(summary)
|
|
187
|
-
|
|
188
|
-
if blockers:
|
|
189
|
-
table = Table(title="Blocking Live-Review Cards")
|
|
190
|
-
table.add_column("Card", style="red")
|
|
191
|
-
table.add_column("Task")
|
|
192
|
-
table.add_column("Summary")
|
|
193
|
-
for card in blockers:
|
|
194
|
-
table.add_row(card["id"], card.get("task_id") or "(unscoped)", card["summary"])
|
|
195
|
-
console.print(table)
|
|
196
|
-
|
|
197
|
-
if payload["pending_signal_items"]:
|
|
198
|
-
table = Table(title="Pending Agent Responses")
|
|
199
|
-
table.add_column("Client", style="cyan")
|
|
200
|
-
table.add_column("Task")
|
|
201
|
-
table.add_column("Transcript", overflow="fold")
|
|
202
|
-
table.add_column("Review Command", overflow="fold")
|
|
203
|
-
for signal in payload["pending_signal_items"]:
|
|
204
|
-
table.add_row(
|
|
205
|
-
signal.get("client") or "",
|
|
206
|
-
signal.get("task_id") or "(unscoped)",
|
|
207
|
-
signal.get("transcript_path") or "",
|
|
208
|
-
signal.get("review_command") or "",
|
|
209
|
-
)
|
|
210
|
-
console.print(table)
|
|
211
|
-
commands = [signal.get("review_command") for signal in payload["pending_signal_items"] if signal.get("review_command")]
|
|
212
|
-
if commands:
|
|
213
|
-
console.print("[cyan]Pending review commands:[/cyan]")
|
|
214
|
-
for command in commands:
|
|
215
|
-
console.print(command)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
@app.command("resolve")
|
|
219
|
-
def resolve(
|
|
220
|
-
card_id: str = typer.Argument(..., help="Critique card ID to update."),
|
|
221
|
-
status: str = typer.Option("resolved", "--status", help="New status: resolved or ignored."),
|
|
222
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
223
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
224
|
-
):
|
|
225
|
-
"""Mark a critique card resolved or ignored after the concern has been handled."""
|
|
226
|
-
if status not in {"resolved", "ignored"}:
|
|
227
|
-
message = "--status must be resolved or ignored."
|
|
228
|
-
if json_format:
|
|
229
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
230
|
-
else:
|
|
231
|
-
console.print(f"[red]{message}[/red]")
|
|
232
|
-
raise typer.Exit(code=2)
|
|
233
|
-
root = project_root.expanduser().resolve()
|
|
234
|
-
card = update_card_status(root, card_id, status) # type: ignore[arg-type]
|
|
235
|
-
if card is None:
|
|
236
|
-
message = f"Critique card {card_id} not found."
|
|
237
|
-
if json_format:
|
|
238
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
239
|
-
else:
|
|
240
|
-
console.print(f"[red]{message}[/red]")
|
|
241
|
-
raise typer.Exit(code=1)
|
|
242
|
-
if json_format:
|
|
243
|
-
typer.echo(json.dumps({"ok": True, "card": card.model_dump()}, indent=2))
|
|
244
|
-
_log_card_resolved(root, card)
|
|
245
|
-
return
|
|
246
|
-
_log_card_resolved(root, card)
|
|
247
|
-
console.print(f"[green]Updated {card.id}:[/green] {card.status}")
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@app.command("repair")
|
|
251
|
-
def repair(
|
|
252
|
-
card_id: str = typer.Argument(..., help="Critique card ID to turn into a repair prompt."),
|
|
253
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
254
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
255
|
-
):
|
|
256
|
-
"""Generate a ready-to-paste repair prompt for a critique card."""
|
|
257
|
-
root = project_root.expanduser().resolve()
|
|
258
|
-
card = get_card(root, card_id)
|
|
259
|
-
if card is None:
|
|
260
|
-
message = f"Critique card {card_id} not found."
|
|
261
|
-
if json_format:
|
|
262
|
-
typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
|
|
263
|
-
else:
|
|
264
|
-
console.print(f"[red]{message}[/red]")
|
|
265
|
-
raise typer.Exit(code=1)
|
|
266
|
-
prompt = build_live_repair_prompt(root, card)
|
|
267
|
-
if json_format:
|
|
268
|
-
typer.echo(json.dumps({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
|
|
269
|
-
return
|
|
270
|
-
typer.echo(prompt)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
@app.command("repair-all")
|
|
274
|
-
def repair_all(
|
|
275
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
276
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Task scope for blocking-card repair prompts."),
|
|
277
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
278
|
-
):
|
|
279
|
-
"""Generate repair prompts for all blocking live-review cards in scope."""
|
|
280
|
-
root = project_root.expanduser().resolve()
|
|
281
|
-
summary = live_review_summary(root, task_id=task_id)
|
|
282
|
-
cards = [
|
|
283
|
-
get_card(root, item["id"])
|
|
284
|
-
for item in summary["blocking_cards"]
|
|
285
|
-
if isinstance(item.get("id"), str)
|
|
286
|
-
]
|
|
287
|
-
resolved_cards = [card for card in cards if card is not None]
|
|
288
|
-
prompt = build_bulk_live_repair_prompt(root, resolved_cards)
|
|
289
|
-
if json_format:
|
|
290
|
-
typer.echo(json.dumps({
|
|
291
|
-
"ok": True,
|
|
292
|
-
"scope_task_id": summary["scope_task_id"],
|
|
293
|
-
"cards": [card.model_dump() for card in resolved_cards],
|
|
294
|
-
"prompt": prompt,
|
|
295
|
-
}, indent=2))
|
|
296
|
-
return
|
|
297
|
-
typer.echo(prompt)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
@app.command("signals")
|
|
301
|
-
def signals(
|
|
302
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
303
|
-
client: str | None = typer.Option(None, "--client", help="Filter by client."),
|
|
304
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
305
|
-
):
|
|
306
|
-
"""List response-ready hook signals waiting for review."""
|
|
307
|
-
root = project_root.expanduser().resolve()
|
|
308
|
-
found = _filtered_signals(root, client)
|
|
309
|
-
if json_format:
|
|
310
|
-
typer.echo(json.dumps({"signals": [item.model_dump() for item in found]}, indent=2))
|
|
311
|
-
return
|
|
312
|
-
table = Table(title="DevCouncil Watch Signals")
|
|
313
|
-
table.add_column("Client", style="cyan")
|
|
314
|
-
table.add_column("Transcript")
|
|
315
|
-
table.add_column("Command")
|
|
316
|
-
table.add_column("Signal")
|
|
317
|
-
for item in found:
|
|
318
|
-
table.add_row(item.client, item.transcript_path or "", item.review_command or "", item.path or "")
|
|
319
|
-
console.print(table)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
@app.command("pending")
|
|
323
|
-
def pending(
|
|
324
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
325
|
-
client: str | None = typer.Option(None, "--client", help="Filter by client."),
|
|
326
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Associate reviewed pending signals with a DevCouncil task."),
|
|
327
|
-
llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
|
|
328
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
329
|
-
keep: bool = typer.Option(False, "--keep", help="Keep signals pending after successful review."),
|
|
330
|
-
force: bool = typer.Option(False, "--force", help="Regenerate and overwrite existing cards for reviewed turns."),
|
|
331
|
-
):
|
|
332
|
-
"""Review every pending response-ready signal that includes a transcript path."""
|
|
333
|
-
root = project_root.expanduser().resolve()
|
|
334
|
-
reviewed = []
|
|
335
|
-
skipped = []
|
|
336
|
-
for signal in _filtered_signals(root, client):
|
|
337
|
-
transcript_path = _resolve_signal_transcript(root, signal)
|
|
338
|
-
if transcript_path is None:
|
|
339
|
-
skipped.append({"signal": signal.model_dump(), "reason": "No transcript path in signal payload."})
|
|
340
|
-
continue
|
|
341
|
-
turn = latest_assistant_turn(transcript_path, client=signal.client)
|
|
342
|
-
if turn is None:
|
|
343
|
-
skipped.append({"signal": signal.model_dump(), "reason": f"No assistant turn found in {transcript_path}."})
|
|
344
|
-
continue
|
|
345
|
-
scoped_task_id = task_id or signal.task_id or active_task_id(root)
|
|
346
|
-
card = asyncio.run(_review_turn(turn, root, signal.client, llm, task_id=scoped_task_id))
|
|
347
|
-
saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
|
|
348
|
-
if saved_path:
|
|
349
|
-
_log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="pending")
|
|
350
|
-
if not keep:
|
|
351
|
-
processed_path = mark_processed(signal, root)
|
|
352
|
-
_log_signal_processed(root, signal, processed_path, card)
|
|
353
|
-
reviewed.append({
|
|
354
|
-
"card": card.model_dump(),
|
|
355
|
-
"path": str(saved_path) if saved_path else None,
|
|
356
|
-
"duplicate": duplicate,
|
|
357
|
-
"signal": signal.model_dump(),
|
|
358
|
-
})
|
|
359
|
-
if not json_format:
|
|
360
|
-
_print_card(card)
|
|
361
|
-
if duplicate:
|
|
362
|
-
console.print(f"[yellow]Already reviewed:[/yellow] {saved_path}")
|
|
363
|
-
else:
|
|
364
|
-
console.print(f"[green]Saved critique card:[/green] {saved_path}")
|
|
365
|
-
|
|
366
|
-
if json_format:
|
|
367
|
-
typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
|
|
368
|
-
return
|
|
369
|
-
for item in skipped:
|
|
370
|
-
console.print(f"[yellow]Skipped signal:[/yellow] {item['reason']}")
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
@app.command("follow")
|
|
374
|
-
def follow(
|
|
375
|
-
transcript: Path | None = typer.Option(None, "--transcript", "-t", help="JSONL transcript to poll."),
|
|
376
|
-
session: str | None = typer.Option(None, "--session", help="Discovered session ID to follow, or 'latest'."),
|
|
377
|
-
latest: bool = typer.Option(False, "--latest", help="Follow the most recently updated discovered session."),
|
|
378
|
-
client: str = typer.Option("generic", "--client", help="Agent client: claude, codex, gemini, cursor, or generic."),
|
|
379
|
-
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
380
|
-
interval: float = typer.Option(2.0, "--interval", help="Polling interval in seconds."),
|
|
381
|
-
llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
|
|
382
|
-
once: bool = typer.Option(False, "--once", help="Review once and exit after a new assistant turn is found."),
|
|
383
|
-
force: bool = typer.Option(False, "--force", help="Regenerate and overwrite an existing card for this turn."),
|
|
384
|
-
task_id: str | None = typer.Option(None, "--task-id", help="Associate critique cards with a DevCouncil task."),
|
|
385
|
-
):
|
|
386
|
-
"""Poll a transcript and emit a critique card whenever the latest assistant turn changes."""
|
|
387
|
-
root = project_root.expanduser().resolve()
|
|
388
|
-
transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
|
|
389
|
-
if transcript_path is None:
|
|
390
|
-
console.print("[red]No transcript selected. Use --transcript, --session, or --latest.[/red]")
|
|
391
|
-
raise typer.Exit(code=2)
|
|
392
|
-
seen_turn_id: str | None = None
|
|
393
|
-
console.print(f"[cyan]Watching transcript:[/cyan] {transcript_path}")
|
|
394
|
-
while True:
|
|
395
|
-
turn = latest_assistant_turn(transcript_path, client=client)
|
|
396
|
-
if turn and turn.turn_id != seen_turn_id:
|
|
397
|
-
seen_turn_id = turn.turn_id
|
|
398
|
-
scoped_task_id = task_id or active_task_id(root)
|
|
399
|
-
card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
|
|
400
|
-
saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
|
|
401
|
-
if saved_path:
|
|
402
|
-
_log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="follow")
|
|
403
|
-
_print_card(card)
|
|
404
|
-
if duplicate:
|
|
405
|
-
console.print(f"[yellow]Already reviewed:[/yellow] {saved_path}")
|
|
406
|
-
else:
|
|
407
|
-
console.print(f"[green]Saved critique card:[/green] {saved_path}")
|
|
408
|
-
if once:
|
|
409
|
-
return
|
|
410
|
-
if once:
|
|
411
|
-
console.print("[yellow]No new assistant turn found.[/yellow]")
|
|
412
|
-
raise typer.Exit(code=1)
|
|
413
|
-
time.sleep(interval)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
@app.command("import")
|
|
417
|
-
def import_transcript(
|
|
418
|
-
transcript: Path = typer.Argument(..., help="Transcript JSONL file to normalize."),
|
|
419
|
-
client: str = typer.Option("generic", "--client", help="Agent client label."),
|
|
420
|
-
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
421
|
-
):
|
|
422
|
-
"""Normalize a transcript into DevCouncil turn records without reviewing it."""
|
|
423
|
-
turns = load_turns(transcript.expanduser().resolve(), client=client)
|
|
424
|
-
if json_format:
|
|
425
|
-
typer.echo(json.dumps({"turns": [turn.model_dump() for turn in turns]}, indent=2))
|
|
426
|
-
return
|
|
427
|
-
console.print(f"Loaded {len(turns)} turns from {transcript}")
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def _print_card(card) -> None:
|
|
431
|
-
color = "green" if card.verdict == "Approved" else "yellow"
|
|
432
|
-
if card.verdict == "Critical Issues":
|
|
433
|
-
color = "red"
|
|
434
|
-
body = [
|
|
435
|
-
f"[bold]Verdict:[/bold] [{color}]{card.verdict}[/{color}]",
|
|
436
|
-
f"[bold]Status:[/bold] {card.status}",
|
|
437
|
-
f"[bold]Task:[/bold] {card.task_id or '(unscoped)'}",
|
|
438
|
-
card.summary,
|
|
439
|
-
]
|
|
440
|
-
if card.concerns:
|
|
441
|
-
body.append("\n[bold]Concerns[/bold]")
|
|
442
|
-
body.extend(f"- {item}" for item in card.concerns)
|
|
443
|
-
if card.alternatives:
|
|
444
|
-
body.append("\n[bold]Alternatives[/bold]")
|
|
445
|
-
body.extend(f"- {item}" for item in card.alternatives)
|
|
446
|
-
if card.evidence_requests:
|
|
447
|
-
body.append("\n[bold]Evidence Requests[/bold]")
|
|
448
|
-
body.extend(f"- {item}" for item in card.evidence_requests)
|
|
449
|
-
body.append(f"\n[bold]Message for agent:[/bold] {card.message_for_agent}")
|
|
450
|
-
console.print(Panel("\n".join(body), title=f"Critique Card {card.id}"))
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def _filtered_signals(root: Path, client: str | None) -> list[ReviewSignal]:
|
|
454
|
-
found = load_signals(root)
|
|
455
|
-
if client is None:
|
|
456
|
-
return found
|
|
457
|
-
normalized = client.lower()
|
|
458
|
-
return [item for item in found if item.client == normalized]
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def _resolve_signal_transcript(root: Path, signal: ReviewSignal) -> Path | None:
|
|
462
|
-
if not signal.transcript_path:
|
|
463
|
-
return None
|
|
464
|
-
path = Path(signal.transcript_path).expanduser()
|
|
465
|
-
if not path.is_absolute():
|
|
466
|
-
path = root / path
|
|
467
|
-
return path.resolve()
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def _resolve_transcript(
|
|
471
|
-
root: Path,
|
|
472
|
-
client: str,
|
|
473
|
-
transcript: Path | None = None,
|
|
474
|
-
session: str | None = None,
|
|
475
|
-
latest: bool = False,
|
|
476
|
-
) -> Path | None:
|
|
477
|
-
if transcript is not None:
|
|
478
|
-
path = transcript.expanduser()
|
|
479
|
-
if not path.is_absolute():
|
|
480
|
-
path = root / path
|
|
481
|
-
return path.resolve()
|
|
482
|
-
|
|
483
|
-
selector = "latest" if latest else session
|
|
484
|
-
if selector is None:
|
|
485
|
-
return None
|
|
486
|
-
|
|
487
|
-
discovered = discover_sessions(root, client=client)
|
|
488
|
-
if not discovered:
|
|
489
|
-
return None
|
|
490
|
-
if selector == "latest":
|
|
491
|
-
return Path(discovered[0].transcript_path).expanduser().resolve()
|
|
492
|
-
for item in discovered:
|
|
493
|
-
if item.id == selector:
|
|
494
|
-
return Path(item.transcript_path).expanduser().resolve()
|
|
495
|
-
return None
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
def _save_card_once(root: Path, card, persist: bool, force: bool) -> tuple[Path | None, bool]:
|
|
499
|
-
if not persist:
|
|
500
|
-
return None, False
|
|
501
|
-
path = card_path(root, card.id)
|
|
502
|
-
if path.exists() and not force:
|
|
503
|
-
return path, True
|
|
504
|
-
if path.exists() and force:
|
|
505
|
-
existing_cards = [item for item in load_cards(root) if item.id == card.id]
|
|
506
|
-
if existing_cards:
|
|
507
|
-
card = card.model_copy(update={"status": existing_cards[0].status})
|
|
508
|
-
return save_card(root, card), False
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def _log_card_reviewed(root: Path, card, path: Path, *, duplicate: bool, source: str) -> None:
|
|
512
|
-
event_type = "live_review_card_reused" if duplicate else "live_review_card_saved"
|
|
513
|
-
TraceLogger(root).log_event(
|
|
514
|
-
event_type,
|
|
515
|
-
{
|
|
516
|
-
"card_id": card.id,
|
|
517
|
-
"client": card.client,
|
|
518
|
-
"verdict": card.verdict,
|
|
519
|
-
"status": card.status,
|
|
520
|
-
"path": str(path),
|
|
521
|
-
"source": source,
|
|
522
|
-
"duplicate": duplicate,
|
|
523
|
-
},
|
|
524
|
-
task_id=card.task_id,
|
|
525
|
-
summary=f"Live-review card {card.id} {'reused' if duplicate else 'saved'} from {source}.",
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
def _log_card_resolved(root: Path, card) -> None:
|
|
530
|
-
TraceLogger(root).log_event(
|
|
531
|
-
"live_review_card_status_updated",
|
|
532
|
-
{
|
|
533
|
-
"card_id": card.id,
|
|
534
|
-
"client": card.client,
|
|
535
|
-
"verdict": card.verdict,
|
|
536
|
-
"status": card.status,
|
|
537
|
-
},
|
|
538
|
-
task_id=card.task_id,
|
|
539
|
-
summary=f"Live-review card {card.id} marked {card.status}.",
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def _log_signal_processed(root: Path, signal: ReviewSignal, processed_path: Path | None, card) -> None:
|
|
544
|
-
TraceLogger(root).log_event(
|
|
545
|
-
"live_review_signal_processed",
|
|
546
|
-
{
|
|
547
|
-
"client": signal.client,
|
|
548
|
-
"signal_path": signal.path,
|
|
549
|
-
"processed_path": str(processed_path) if processed_path else None,
|
|
550
|
-
"transcript_path": signal.transcript_path,
|
|
551
|
-
"card_id": card.id,
|
|
552
|
-
},
|
|
553
|
-
task_id=card.task_id or signal.task_id,
|
|
554
|
-
summary=f"Live-review signal processed into card {card.id}.",
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: str | None = None):
|
|
559
|
-
if not use_llm:
|
|
560
|
-
# Pass task_id so the deterministic review can ground completion/evidence
|
|
561
|
-
# claims against the task's real verification state.
|
|
562
|
-
return review_turn(turn, root, client=client, task_id=task_id)
|
|
563
|
-
try:
|
|
564
|
-
config = load_config(root)
|
|
565
|
-
validate_model_provider(config.models.provider)
|
|
566
|
-
api_key = get_api_key(config.models.provider, root)
|
|
567
|
-
provider = create_provider(config.models.provider, api_key, project_root=root)
|
|
568
|
-
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
569
|
-
router = ModelRouter(provider, role_config, project_root=root)
|
|
570
|
-
except Exception as exc:
|
|
571
|
-
console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
|
|
572
|
-
return review_turn(turn, root, client=client, task_id=task_id)
|
|
573
|
-
card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
|
|
574
|
-
return card.model_copy(update={"task_id": task_id}) if task_id else card
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import typer
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from rich.console import Console
|
|
5
|
-
|
|
6
|
-
from devcouncil.cli.commands.init import initialize_project
|
|
7
|
-
from devcouncil.execution.fs_watcher import FilesystemWatcher
|
|
8
|
-
|
|
9
|
-
console = Console()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def watch_fs(
|
|
13
|
-
task_id: str = typer.Option(..., "--task", help="Task ID to attribute file changes to."),
|
|
14
|
-
poll_interval: float = typer.Option(1.0, "--poll-interval"),
|
|
15
|
-
once: bool = typer.Option(False, "--once", help="Scan once and exit."),
|
|
16
|
-
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
17
|
-
json_format: bool = typer.Option(False, "--json"),
|
|
18
|
-
):
|
|
19
|
-
root = project_root.expanduser().resolve()
|
|
20
|
-
initialize_project(root, quiet=True)
|
|
21
|
-
|
|
22
|
-
def _print_event(event: dict) -> None:
|
|
23
|
-
status = "allowed" if event["allowed"] else "denied"
|
|
24
|
-
console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
|
|
25
|
-
|
|
26
|
-
watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
|
|
27
|
-
if once:
|
|
28
|
-
events = watcher.scan_once()
|
|
29
|
-
if json_format:
|
|
30
|
-
typer.echo(json.dumps({"events": events}, indent=2))
|
|
31
|
-
else:
|
|
32
|
-
for event in events:
|
|
33
|
-
status = "allowed" if event["allowed"] else "denied"
|
|
34
|
-
console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
|
|
35
|
-
return
|
|
36
|
-
console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
|
|
37
|
-
try:
|
|
38
|
-
watcher.watch()
|
|
39
|
-
except KeyboardInterrupt:
|
|
40
|
-
console.print("[yellow]Stopped filesystem watcher.[/yellow]")
|