devcouncil 0.1.1 → 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 +218 -96
- package/bin/devcouncil.js +130 -32
- package/package.json +7 -11
- package/pyproject.toml +0 -34
- 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 -158
- 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 -143
- 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/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +0 -51
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/config.py +0 -76
- package/src/devcouncil/cli/commands/dashboard.py +0 -26
- package/src/devcouncil/cli/commands/doctor.py +0 -140
- package/src/devcouncil/cli/commands/go.py +0 -237
- package/src/devcouncil/cli/commands/hook.py +0 -128
- package/src/devcouncil/cli/commands/init.py +0 -153
- package/src/devcouncil/cli/commands/integrate.py +0 -726
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -42
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -327
- package/src/devcouncil/cli/commands/prompt.py +0 -50
- package/src/devcouncil/cli/commands/repair.py +0 -72
- package/src/devcouncil/cli/commands/report.py +0 -129
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
- package/src/devcouncil/cli/commands/rollback.py +0 -60
- package/src/devcouncil/cli/commands/run.py +0 -269
- package/src/devcouncil/cli/commands/setup.py +0 -223
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/status.py +0 -117
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -44
- package/src/devcouncil/cli/commands/verify.py +0 -193
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/main.py +0 -88
- 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 -27
- package/src/devcouncil/domain/gap.py +0 -26
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -26
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/hook_policy.py +0 -165
- package/src/devcouncil/execution/patch.py +0 -28
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -92
- package/src/devcouncil/execution/prompt_builder.py +0 -59
- package/src/devcouncil/execution/task_runner.py +0 -166
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/coding_cli.py +0 -112
- 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 -107
- 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 -50
- 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 -34
- package/src/devcouncil/gating/policy.py +0 -190
- 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 -120
- package/src/devcouncil/indexing/repo_mapper.py +0 -208
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/gitnexus.py +0 -27
- 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 -599
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -207
- 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 -138
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -38
- package/src/devcouncil/llm/provider.py +0 -146
- package/src/devcouncil/llm/router.py +0 -125
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/critique_service.py +0 -66
- package/src/devcouncil/planning/plan_service.py +0 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -44
- package/src/devcouncil/repo/__init__.py +0 -0
- 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 -20
- package/src/devcouncil/reporting/markdown_report.py +0 -68
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -66
- package/src/devcouncil/storage/models.py +0 -83
- package/src/devcouncil/storage/repositories.py +0 -423
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -34
- package/src/devcouncil/telemetry/traces.py +0 -91
- package/src/devcouncil/telemetry/tracker.py +0 -49
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -122
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -141
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/verifier.py +0 -530
- package/uv.lock +0 -1085
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Requirements:
|
|
2
|
-
{requirements_json}
|
|
3
|
-
|
|
4
|
-
Target Plan:
|
|
5
|
-
{target_plan_json}
|
|
6
|
-
|
|
7
|
-
You are a hostile staff engineer reviewing another team's implementation plan.
|
|
8
|
-
Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
|
|
9
|
-
Do not praise. Do not rewrite the plan.
|
|
10
|
-
Every finding must include a falsifiable_check.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Requirements:
|
|
2
|
-
{requirements_json}
|
|
3
|
-
|
|
4
|
-
Target Plan:
|
|
5
|
-
{target_plan_json}
|
|
6
|
-
|
|
7
|
-
You are a hostile staff engineer reviewing another team's implementation plan.
|
|
8
|
-
Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
|
|
9
|
-
Do not praise. Do not rewrite the plan.
|
|
10
|
-
Every finding must include a falsifiable_check.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
You are an expert software reviewer. Review the following code changes against the task requirements.
|
|
2
|
-
Task: {task.title}
|
|
3
|
-
Description: {task.description}
|
|
4
|
-
|
|
5
|
-
Requirements:
|
|
6
|
-
{requirements_json}
|
|
7
|
-
|
|
8
|
-
Code Diff:
|
|
9
|
-
{diff}
|
|
10
|
-
|
|
11
|
-
Your task is to identify if the implementation is complete, correct, and follows best practices.
|
|
12
|
-
- Identify missing edge cases.
|
|
13
|
-
- Identify architectural drift.
|
|
14
|
-
- Identify security risks not caught by static scans.
|
|
15
|
-
|
|
16
|
-
Return a JSON object with 'is_satisfactory' and a list of 'findings' (as Gap objects).
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Goal: {goal}
|
|
2
|
-
|
|
3
|
-
Requirements:
|
|
4
|
-
{requirements_json}
|
|
5
|
-
|
|
6
|
-
Repository Map:
|
|
7
|
-
{repo_map_json}
|
|
8
|
-
|
|
9
|
-
Your task is to create a detailed implementation plan.
|
|
10
|
-
- Break down the requirements into atomic implementation tasks.
|
|
11
|
-
- For each task, specify which files will be created or modified.
|
|
12
|
-
- Specify which tests are expected to verify the task.
|
|
13
|
-
- Ensure each task maps back to at least one requirement.
|
|
14
|
-
|
|
15
|
-
Role-specific instructions:
|
|
16
|
-
You are the pragmatic tech lead. Optimize for simplicity and minimal dependencies.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Goal: {goal}
|
|
2
|
-
|
|
3
|
-
Requirements:
|
|
4
|
-
{requirements_json}
|
|
5
|
-
|
|
6
|
-
Repository Map:
|
|
7
|
-
{repo_map_json}
|
|
8
|
-
|
|
9
|
-
Your task is to create a detailed implementation plan.
|
|
10
|
-
- Break down the requirements into atomic implementation tasks.
|
|
11
|
-
- For each task, specify which files will be created or modified.
|
|
12
|
-
- Specify which tests are expected to verify the task.
|
|
13
|
-
- Ensure each task maps back to at least one requirement.
|
|
14
|
-
|
|
15
|
-
Role-specific instructions:
|
|
16
|
-
You are the production-readiness architect. Optimize for security, performance, edge cases, failure modes, and maintainability. Assume the first plan will miss subtle requirements.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
Original Plan:
|
|
2
|
-
{original_plan_json}
|
|
3
|
-
|
|
4
|
-
Critique Findings:
|
|
5
|
-
{findings_json}
|
|
6
|
-
|
|
7
|
-
You are the planner who created the original plan. Review the critique findings.
|
|
8
|
-
- A finding can be rejected only with artifact evidence or strong justification.
|
|
9
|
-
- A finding can be accepted and converted into a requirement/task/test.
|
|
10
|
-
- No hand-wavy rebuttals.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Goal: {goal}
|
|
2
|
-
|
|
3
|
-
Repository Map:
|
|
4
|
-
{repo_map_json}
|
|
5
|
-
|
|
6
|
-
Your task is to draft the initial software specification for this goal.
|
|
7
|
-
1. Identify functional and non-functional requirements.
|
|
8
|
-
2. Extract any assumptions you are making about the codebase or architecture.
|
|
9
|
-
3. List any blocking questions that the user must answer before implementation can proceed.
|
|
10
|
-
|
|
11
|
-
Each requirement MUST have clear acceptance criteria with verification methods.
|
|
12
|
-
Each assumption MUST have a confidence and impact level.
|
|
File without changes
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Literal, List
|
|
3
|
-
|
|
4
|
-
class Assumption(BaseModel):
|
|
5
|
-
id: str
|
|
6
|
-
statement: str
|
|
7
|
-
confidence: Literal["low", "medium", "high"]
|
|
8
|
-
impact: Literal["low", "medium", "high"]
|
|
9
|
-
reversible: bool
|
|
10
|
-
requires_user_confirmation: bool
|
|
11
|
-
linked_requirement_ids: List[str] = Field(default_factory=list)
|
|
12
|
-
status: Literal[
|
|
13
|
-
"open",
|
|
14
|
-
"confirmed",
|
|
15
|
-
"rejected",
|
|
16
|
-
"converted_to_requirement"
|
|
17
|
-
] = "open"
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
from typing import Literal, Optional
|
|
3
|
-
|
|
4
|
-
class CritiqueFinding(BaseModel):
|
|
5
|
-
id: str
|
|
6
|
-
source_agent: str
|
|
7
|
-
target_plan_id: str
|
|
8
|
-
severity: Literal["low", "medium", "high", "critical"]
|
|
9
|
-
finding_type: Literal[
|
|
10
|
-
"missing_requirement",
|
|
11
|
-
"missing_task",
|
|
12
|
-
"missing_test",
|
|
13
|
-
"bad_assumption",
|
|
14
|
-
"architecture_risk",
|
|
15
|
-
"security_risk",
|
|
16
|
-
"performance_risk",
|
|
17
|
-
"dependency_risk",
|
|
18
|
-
"migration_risk",
|
|
19
|
-
"unverifiable_acceptance_criteria"
|
|
20
|
-
]
|
|
21
|
-
claim: str
|
|
22
|
-
linked_requirement_id: Optional[str] = None
|
|
23
|
-
suggested_requirement: Optional[str] = None
|
|
24
|
-
suggested_task: Optional[str] = None
|
|
25
|
-
falsifiable_check: str
|
|
26
|
-
status: Literal[
|
|
27
|
-
"open",
|
|
28
|
-
"accepted",
|
|
29
|
-
"rejected",
|
|
30
|
-
"converted",
|
|
31
|
-
"needs_user"
|
|
32
|
-
] = "open"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
from typing import Literal, List
|
|
3
|
-
|
|
4
|
-
class CommandResult(BaseModel):
|
|
5
|
-
command: str
|
|
6
|
-
exit_code: int
|
|
7
|
-
stdout_path: str
|
|
8
|
-
stderr_path: str
|
|
9
|
-
summary: str
|
|
10
|
-
|
|
11
|
-
class DiffEvidence(BaseModel):
|
|
12
|
-
task_id: str
|
|
13
|
-
changed_files: List[str]
|
|
14
|
-
added_files: List[str]
|
|
15
|
-
deleted_files: List[str]
|
|
16
|
-
diff_summary: str
|
|
17
|
-
|
|
18
|
-
class VerificationEvidence(BaseModel):
|
|
19
|
-
__test__ = False # Prevent pytest from collecting this as a test class
|
|
20
|
-
requirement_id: str
|
|
21
|
-
acceptance_criterion_id: str
|
|
22
|
-
command: str
|
|
23
|
-
status: Literal["passed", "failed", "not_run"]
|
|
24
|
-
evidence_summary: str
|
|
25
|
-
|
|
26
|
-
# Backward-compatible alias
|
|
27
|
-
TestEvidence = VerificationEvidence
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Literal, List, Optional
|
|
3
|
-
|
|
4
|
-
class Gap(BaseModel):
|
|
5
|
-
id: str
|
|
6
|
-
severity: Literal["low", "medium", "high", "critical"]
|
|
7
|
-
gap_type: Literal[
|
|
8
|
-
"requirement_not_planned",
|
|
9
|
-
"task_not_implemented",
|
|
10
|
-
"planned_file_not_changed",
|
|
11
|
-
"orphan_diff",
|
|
12
|
-
"missing_test",
|
|
13
|
-
"test_failed",
|
|
14
|
-
"acceptance_criteria_unproven",
|
|
15
|
-
"assumption_violated",
|
|
16
|
-
"architecture_drift",
|
|
17
|
-
"security_risk",
|
|
18
|
-
"dependency_risk",
|
|
19
|
-
"migration_gap"
|
|
20
|
-
]
|
|
21
|
-
requirement_id: Optional[str] = None
|
|
22
|
-
task_id: Optional[str] = None
|
|
23
|
-
description: str
|
|
24
|
-
evidence: List[str] = Field(default_factory=list)
|
|
25
|
-
recommended_fix: str
|
|
26
|
-
blocking: bool
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Literal, List
|
|
3
|
-
|
|
4
|
-
class AcceptanceCriterion(BaseModel):
|
|
5
|
-
id: str
|
|
6
|
-
description: str
|
|
7
|
-
verification_method: Literal[
|
|
8
|
-
"unit_test",
|
|
9
|
-
"integration_test",
|
|
10
|
-
"manual",
|
|
11
|
-
"static_check",
|
|
12
|
-
"llm_review"
|
|
13
|
-
]
|
|
14
|
-
required: bool = True
|
|
15
|
-
|
|
16
|
-
class Requirement(BaseModel):
|
|
17
|
-
id: str
|
|
18
|
-
title: str
|
|
19
|
-
description: str
|
|
20
|
-
priority: Literal["low", "medium", "high", "critical"]
|
|
21
|
-
source: Literal["user", "planner", "critic", "arbiter"]
|
|
22
|
-
acceptance_criteria: List[AcceptanceCriterion] = Field(default_factory=list)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Literal, List
|
|
3
|
-
|
|
4
|
-
class PlannedFile(BaseModel):
|
|
5
|
-
path: str
|
|
6
|
-
reason: str
|
|
7
|
-
allowed_change: Literal["create", "modify", "delete", "read_only"]
|
|
8
|
-
|
|
9
|
-
class Task(BaseModel):
|
|
10
|
-
id: str
|
|
11
|
-
title: str
|
|
12
|
-
description: str
|
|
13
|
-
requirement_ids: List[str] = Field(default_factory=list)
|
|
14
|
-
acceptance_criterion_ids: List[str] = Field(default_factory=list)
|
|
15
|
-
planned_files: List[PlannedFile] = Field(default_factory=list)
|
|
16
|
-
expected_tests: List[str] = Field(default_factory=list)
|
|
17
|
-
allowed_commands: List[str] = Field(default_factory=list)
|
|
18
|
-
forbidden_changes: List[str] = Field(default_factory=list)
|
|
19
|
-
status: Literal[
|
|
20
|
-
"planned",
|
|
21
|
-
"ready",
|
|
22
|
-
"running",
|
|
23
|
-
"blocked",
|
|
24
|
-
"verified",
|
|
25
|
-
"done"
|
|
26
|
-
] = "planned"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import List
|
|
3
|
-
from devcouncil.domain.task import Task
|
|
4
|
-
from devcouncil.domain.requirement import Requirement
|
|
5
|
-
from devcouncil.utils.redaction import redact_string
|
|
6
|
-
import json
|
|
7
|
-
|
|
8
|
-
class ContextBuilder:
|
|
9
|
-
"""Gathers repo-level and task-level context for agent prompts."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, project_root: Path):
|
|
12
|
-
self.project_root = project_root
|
|
13
|
-
|
|
14
|
-
def build_task_context(self, task: Task, requirements: List[Requirement]) -> str:
|
|
15
|
-
"""Collects all relevant information for implementing a task."""
|
|
16
|
-
|
|
17
|
-
# 1. Map requirements relevant to this task
|
|
18
|
-
req_map = {r.id: r for r in requirements}
|
|
19
|
-
task_reqs = [req_map[rid] for rid in task.requirement_ids if rid in req_map]
|
|
20
|
-
|
|
21
|
-
# 2. Gather content of planned files (if they exist)
|
|
22
|
-
file_contents = {}
|
|
23
|
-
for pf in task.planned_files:
|
|
24
|
-
file_path = self.project_root / pf.path
|
|
25
|
-
if file_path.exists() and file_path.is_file():
|
|
26
|
-
try:
|
|
27
|
-
raw = file_path.read_text(encoding="utf-8")
|
|
28
|
-
file_contents[pf.path] = redact_string(raw)
|
|
29
|
-
except Exception:
|
|
30
|
-
file_contents[pf.path] = "[Error reading file]"
|
|
31
|
-
else:
|
|
32
|
-
file_contents[pf.path] = "[New file - does not exist yet]"
|
|
33
|
-
|
|
34
|
-
# 3. Assemble the context string
|
|
35
|
-
context = {
|
|
36
|
-
"task": task.model_dump(),
|
|
37
|
-
"relevant_requirements": [r.model_dump() for r in task_reqs],
|
|
38
|
-
"file_contents": file_contents,
|
|
39
|
-
"project_structure": self.get_structure_summary(task)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return json.dumps(context, indent=2)
|
|
43
|
-
|
|
44
|
-
def get_structure_summary(self, task: Task = None) -> List[str]:
|
|
45
|
-
"""Simple list of files in the project for context."""
|
|
46
|
-
try:
|
|
47
|
-
import subprocess
|
|
48
|
-
output = subprocess.check_output(
|
|
49
|
-
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
50
|
-
cwd=self.project_root,
|
|
51
|
-
stderr=subprocess.DEVNULL
|
|
52
|
-
).decode().splitlines()
|
|
53
|
-
|
|
54
|
-
if task and task.planned_files:
|
|
55
|
-
planned_paths = {pf.path for pf in task.planned_files}
|
|
56
|
-
output = [p for p in output if p in planned_paths] + [p for p in output if p not in planned_paths]
|
|
57
|
-
|
|
58
|
-
return output[:100] # Limit to avoid context overflow
|
|
59
|
-
except Exception:
|
|
60
|
-
return []
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import List
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
from devcouncil.domain.task import Task
|
|
5
|
-
from devcouncil.domain.requirement import Requirement
|
|
6
|
-
|
|
7
|
-
class ExecutionResult(BaseModel):
|
|
8
|
-
success: bool
|
|
9
|
-
message: str
|
|
10
|
-
|
|
11
|
-
class Executor(ABC):
|
|
12
|
-
@abstractmethod
|
|
13
|
-
def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
14
|
-
"""Execute the task and return the result."""
|
|
15
|
-
pass
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import fnmatch
|
|
2
|
-
import re
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from pathlib import PurePosixPath
|
|
6
|
-
from typing import Any, Optional
|
|
7
|
-
|
|
8
|
-
from devcouncil.domain.task import Task
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass(frozen=True)
|
|
12
|
-
class HookDecision:
|
|
13
|
-
action: str
|
|
14
|
-
reason: str
|
|
15
|
-
target: Optional[str] = None
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def allowed(self) -> bool:
|
|
19
|
-
return self.action in {"allow", "warn"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class HookPolicy:
|
|
23
|
-
"""Policy-backed hook checks for coding CLI tool-use events."""
|
|
24
|
-
|
|
25
|
-
def __init__(self, project_root: Path | None = None):
|
|
26
|
-
self.project_root = project_root.resolve() if project_root else None
|
|
27
|
-
|
|
28
|
-
secret_path_patterns = (
|
|
29
|
-
".env",
|
|
30
|
-
".env.*",
|
|
31
|
-
"**/.env",
|
|
32
|
-
"**/.env.*",
|
|
33
|
-
"**/credentials/**",
|
|
34
|
-
"**/secrets/**",
|
|
35
|
-
"**/*.pem",
|
|
36
|
-
"**/*.key",
|
|
37
|
-
)
|
|
38
|
-
protected_path_patterns = (
|
|
39
|
-
"package.json",
|
|
40
|
-
"pyproject.toml",
|
|
41
|
-
"uv.lock",
|
|
42
|
-
"Dockerfile",
|
|
43
|
-
"docker-compose.yml",
|
|
44
|
-
".github/workflows/*.yml",
|
|
45
|
-
".github/workflows/*.yaml",
|
|
46
|
-
"schema.prisma",
|
|
47
|
-
"wrangler.toml",
|
|
48
|
-
"index.html",
|
|
49
|
-
)
|
|
50
|
-
write_tools = {
|
|
51
|
-
"apply_patch",
|
|
52
|
-
"edit",
|
|
53
|
-
"edit_file",
|
|
54
|
-
"replace",
|
|
55
|
-
"write",
|
|
56
|
-
"write_file",
|
|
57
|
-
"Edit",
|
|
58
|
-
"MultiEdit",
|
|
59
|
-
"Write",
|
|
60
|
-
}
|
|
61
|
-
shell_tools = {
|
|
62
|
-
"bash",
|
|
63
|
-
"exec",
|
|
64
|
-
"exec_command",
|
|
65
|
-
"local_shell",
|
|
66
|
-
"run_command",
|
|
67
|
-
"run_shell_command",
|
|
68
|
-
"shell",
|
|
69
|
-
"shell_command",
|
|
70
|
-
"Bash",
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
def evaluate(self, call_data: dict[str, Any], active_task: Optional[Task]) -> HookDecision:
|
|
74
|
-
tool_name = str(call_data.get("name") or call_data.get("tool_name") or call_data.get("tool") or "")
|
|
75
|
-
arguments = call_data.get("arguments") or call_data.get("input") or call_data.get("tool_input") or {}
|
|
76
|
-
if not isinstance(arguments, dict):
|
|
77
|
-
arguments = {}
|
|
78
|
-
|
|
79
|
-
if tool_name in self.shell_tools:
|
|
80
|
-
command = self._extract_command(arguments)
|
|
81
|
-
return self.evaluate_command(command)
|
|
82
|
-
|
|
83
|
-
if tool_name in self.write_tools:
|
|
84
|
-
target = self._extract_path(arguments)
|
|
85
|
-
return self.evaluate_file_write(target, active_task)
|
|
86
|
-
|
|
87
|
-
return HookDecision("allow", "Tool is outside DevCouncil hook policy.")
|
|
88
|
-
|
|
89
|
-
def evaluate_command(self, command: str) -> HookDecision:
|
|
90
|
-
normalized = " ".join(command.split())
|
|
91
|
-
lowered = normalized.lower()
|
|
92
|
-
if not normalized:
|
|
93
|
-
return HookDecision("allow", "No command detected.")
|
|
94
|
-
|
|
95
|
-
if "--no-verify" in lowered or "--no-gpg-sign" in lowered:
|
|
96
|
-
return HookDecision("deny", "Verification bypass flags are not allowed.", normalized)
|
|
97
|
-
|
|
98
|
-
if re.search(r"\bgit\s+reset\s+--hard\s+(origin/)?(main|master)\b", lowered):
|
|
99
|
-
return HookDecision("deny", "Protected branch hard resets are not allowed.", normalized)
|
|
100
|
-
|
|
101
|
-
if re.search(r"\bgit\s+push\b.*(\s--force(?:-with-lease)?\b|\s-f\b)", lowered):
|
|
102
|
-
return HookDecision("deny", "Force pushes are not allowed.", normalized)
|
|
103
|
-
|
|
104
|
-
if re.search(r"\bgit\s+push\s+\S+\s+((head:)?(main|master)|(main|master):\S+)\b", lowered):
|
|
105
|
-
return HookDecision("warn", "Direct pushes to protected branches should go through verification gates.", normalized)
|
|
106
|
-
|
|
107
|
-
return HookDecision("allow", "Command is allowed.", normalized)
|
|
108
|
-
|
|
109
|
-
def evaluate_file_write(self, raw_path: Optional[str], active_task: Optional[Task]) -> HookDecision:
|
|
110
|
-
if not raw_path:
|
|
111
|
-
return HookDecision("allow", "No file path detected.")
|
|
112
|
-
|
|
113
|
-
path = self._normalize_path(raw_path)
|
|
114
|
-
if self._matches_any(path, self.secret_path_patterns):
|
|
115
|
-
return HookDecision("deny", "Secret and credential paths are never writable through hooks.", path)
|
|
116
|
-
|
|
117
|
-
if active_task is None:
|
|
118
|
-
return HookDecision("deny", "No running DevCouncil task authorizes this file write.", path)
|
|
119
|
-
|
|
120
|
-
if active_task and not self._is_planned_file(path, active_task):
|
|
121
|
-
return HookDecision("deny", f"Task {active_task.id} does not authorize changes to {path}.", path)
|
|
122
|
-
|
|
123
|
-
if self._matches_any(path, self.protected_path_patterns):
|
|
124
|
-
return HookDecision("warn", f"{path} is a protected high-impact file; verification gates must approve it.", path)
|
|
125
|
-
|
|
126
|
-
return HookDecision("allow", "File write is allowed.", path)
|
|
127
|
-
|
|
128
|
-
def _extract_command(self, arguments: dict[str, Any]) -> str:
|
|
129
|
-
value = arguments.get("command") or arguments.get("cmd") or arguments.get("script") or ""
|
|
130
|
-
return str(value)
|
|
131
|
-
|
|
132
|
-
def _extract_path(self, arguments: dict[str, Any]) -> Optional[str]:
|
|
133
|
-
value = (
|
|
134
|
-
arguments.get("path")
|
|
135
|
-
or arguments.get("file_path")
|
|
136
|
-
or arguments.get("filepath")
|
|
137
|
-
or arguments.get("filePath")
|
|
138
|
-
or arguments.get("target")
|
|
139
|
-
or arguments.get("target_file")
|
|
140
|
-
)
|
|
141
|
-
return str(value) if value else None
|
|
142
|
-
|
|
143
|
-
def _normalize_path(self, raw_path: str) -> str:
|
|
144
|
-
path = raw_path.strip().strip('"').replace("\\", "/")
|
|
145
|
-
if self.project_root:
|
|
146
|
-
try:
|
|
147
|
-
candidate = Path(path)
|
|
148
|
-
resolved = candidate.resolve() if candidate.is_absolute() else (self.project_root / path).resolve()
|
|
149
|
-
return resolved.relative_to(self.project_root).as_posix()
|
|
150
|
-
except (OSError, ValueError):
|
|
151
|
-
pass
|
|
152
|
-
if re.match(r"^[A-Za-z]:/", path):
|
|
153
|
-
parts = PurePosixPath(path).parts
|
|
154
|
-
path = "/".join(parts[1:])
|
|
155
|
-
return path[2:] if path.startswith("./") else path
|
|
156
|
-
|
|
157
|
-
def _is_planned_file(self, path: str, task: Task) -> bool:
|
|
158
|
-
for planned in task.planned_files:
|
|
159
|
-
planned_path = self._normalize_path(planned.path)
|
|
160
|
-
if path == planned_path or fnmatch.fnmatch(path, planned_path):
|
|
161
|
-
return True
|
|
162
|
-
return False
|
|
163
|
-
|
|
164
|
-
def _matches_any(self, path: str, patterns: tuple[str, ...]) -> bool:
|
|
165
|
-
return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from devcouncil.app.errors import ExecutionError
|
|
4
|
-
|
|
5
|
-
class PatchEngine:
|
|
6
|
-
"""Handles applying unified diff patches to the codebase."""
|
|
7
|
-
|
|
8
|
-
def __init__(self, project_root: Path):
|
|
9
|
-
self.project_root = project_root
|
|
10
|
-
|
|
11
|
-
def apply_patch(self, patch_content: str) -> bool:
|
|
12
|
-
"""Applies a git-style patch to the repository."""
|
|
13
|
-
patch_file = self.project_root / ".devcouncil" / "temp.patch"
|
|
14
|
-
patch_file.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
-
patch_file.write_text(patch_content, encoding="utf-8")
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
# Using git apply for robust patch application
|
|
19
|
-
subprocess.check_call(
|
|
20
|
-
["git", "apply", "--ignore-whitespace", str(patch_file)],
|
|
21
|
-
cwd=self.project_root
|
|
22
|
-
)
|
|
23
|
-
return True
|
|
24
|
-
except subprocess.CalledProcessError as e:
|
|
25
|
-
raise ExecutionError(f"Failed to apply patch: {e}")
|
|
26
|
-
finally:
|
|
27
|
-
if patch_file.exists():
|
|
28
|
-
patch_file.unlink()
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from devcouncil.app.errors import ExecutionError
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def resolve_project_path(project_root: Path, path: str) -> Path:
|
|
7
|
-
"""Resolve a repository-relative path and reject paths outside the project."""
|
|
8
|
-
root_resolved = project_root.resolve()
|
|
9
|
-
resolved = (root_resolved / path).resolve()
|
|
10
|
-
try:
|
|
11
|
-
resolved.relative_to(root_resolved)
|
|
12
|
-
except ValueError as exc:
|
|
13
|
-
raise ExecutionError(f"Path traversal blocked: {path} resolves outside project root.") from exc
|
|
14
|
-
return resolved
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import fnmatch
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List, Literal, Optional
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
-
from devcouncil.domain.task import PlannedFile, Task
|
|
6
|
-
from devcouncil.app.errors import GatingError
|
|
7
|
-
|
|
8
|
-
class PermissionPolicy(BaseModel):
|
|
9
|
-
"""Defines the security boundaries for task execution."""
|
|
10
|
-
allow_file_create: bool = False
|
|
11
|
-
allow_file_delete: bool = False
|
|
12
|
-
allowed_shell_commands: List[str] = Field(default_factory=list)
|
|
13
|
-
restricted_paths: List[str] = Field(default_factory=lambda: [".git/*", ".devcouncil/*", ".env*"])
|
|
14
|
-
|
|
15
|
-
class PermissionManager:
|
|
16
|
-
def __init__(self, policy: PermissionPolicy, project_root: Path = Path(".")):
|
|
17
|
-
self.policy = policy
|
|
18
|
-
self.project_root = project_root
|
|
19
|
-
self.dynamic_ignores = self._load_devcouncilignore()
|
|
20
|
-
|
|
21
|
-
def _load_devcouncilignore(self) -> List[str]:
|
|
22
|
-
"""Load additional restricted paths from .devcouncilignore."""
|
|
23
|
-
ignore_file = self.project_root / ".devcouncilignore"
|
|
24
|
-
if ignore_file.exists():
|
|
25
|
-
try:
|
|
26
|
-
lines = ignore_file.read_text().splitlines()
|
|
27
|
-
return [line.strip() for line in lines if line.strip() and not line.startswith("#")]
|
|
28
|
-
except Exception:
|
|
29
|
-
pass
|
|
30
|
-
return []
|
|
31
|
-
|
|
32
|
-
def is_file_change_allowed(
|
|
33
|
-
self,
|
|
34
|
-
path: str,
|
|
35
|
-
task: Task,
|
|
36
|
-
operation: Literal["create", "modify", "delete", "write"] = "write",
|
|
37
|
-
) -> bool:
|
|
38
|
-
"""Check if a file change is authorized by the task or policy."""
|
|
39
|
-
# 1. Check restricted paths (e.g. .git) and dynamic ignores
|
|
40
|
-
all_restricted = self.policy.restricted_paths + self.dynamic_ignores
|
|
41
|
-
for restricted in all_restricted:
|
|
42
|
-
if fnmatch.fnmatch(path, restricted) or path.startswith(restricted.strip("*")):
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
# 2. Check if path is in task's planned files with a compatible operation.
|
|
46
|
-
planned = self._planned_file_for(path, task)
|
|
47
|
-
if not planned:
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
if planned.allowed_change == "read_only":
|
|
51
|
-
return False
|
|
52
|
-
if operation == "write":
|
|
53
|
-
return planned.allowed_change in {"create", "modify"}
|
|
54
|
-
return planned.allowed_change == operation
|
|
55
|
-
|
|
56
|
-
def _planned_file_for(self, path: str, task: Task) -> Optional[PlannedFile]:
|
|
57
|
-
normalized = path.replace("\\", "/")
|
|
58
|
-
for planned in task.planned_files:
|
|
59
|
-
planned_path = planned.path.replace("\\", "/")
|
|
60
|
-
if normalized == planned_path or fnmatch.fnmatch(normalized, planned_path):
|
|
61
|
-
return planned
|
|
62
|
-
return None
|
|
63
|
-
|
|
64
|
-
def is_command_allowed(self, command: str, task: Task) -> bool:
|
|
65
|
-
"""Check if a shell command is authorized by the task or global allowlist."""
|
|
66
|
-
# 1. Check task-specific allowlist
|
|
67
|
-
if any(fnmatch.fnmatch(command, allowed) for allowed in task.allowed_commands):
|
|
68
|
-
return True
|
|
69
|
-
|
|
70
|
-
# 2. Check global policy allowlist
|
|
71
|
-
if any(fnmatch.fnmatch(command, allowed) for allowed in self.policy.allowed_shell_commands):
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
return False
|
|
75
|
-
|
|
76
|
-
def validate_action(
|
|
77
|
-
self,
|
|
78
|
-
action_type: str,
|
|
79
|
-
target: str,
|
|
80
|
-
task: Task,
|
|
81
|
-
operation: Literal["create", "modify", "delete", "write"] = "write",
|
|
82
|
-
):
|
|
83
|
-
"""Raise GatingError if an execution action violates permissions."""
|
|
84
|
-
if action_type == "file_write":
|
|
85
|
-
if not self.is_file_change_allowed(target, task, operation):
|
|
86
|
-
raise GatingError(
|
|
87
|
-
f"Unauthorized file {operation}: {target}. "
|
|
88
|
-
"File and operation must match task planned_files."
|
|
89
|
-
)
|
|
90
|
-
elif action_type == "shell":
|
|
91
|
-
if not self.is_command_allowed(target, task):
|
|
92
|
-
raise GatingError(f"Unauthorized shell command: {target}. Command must be in task's allowed_commands.")
|