devcouncil 0.1.0 → 0.2.0
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/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +330 -0
- package/src/devcouncil/storage/db.py +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -1,45 +1,52 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import logging
|
|
3
|
-
from devcouncil.domain.gap import Gap
|
|
4
|
-
|
|
5
|
-
logger = logging.getLogger(__name__)
|
|
6
|
-
|
|
7
|
-
class CleanGitCheck:
|
|
8
|
-
"""Ensures the working tree is clean before a task starts."""
|
|
9
|
-
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
import subprocess
|
|
2
|
+
import logging
|
|
3
|
+
from devcouncil.domain.gap import Gap
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class CleanGitCheck:
|
|
8
|
+
"""Ensures the working tree is clean before a task starts."""
|
|
9
|
+
|
|
10
|
+
def _is_runtime_state(self, line: str) -> bool:
|
|
11
|
+
path = line[3:].strip().replace("\\", "/")
|
|
12
|
+
# DevCouncil manages the root .gitignore itself (ensure_gitignore runs on
|
|
13
|
+
# init and before every task), so it must not block execution.
|
|
14
|
+
return path.startswith(".devcouncil/") or path == ".gitignore"
|
|
15
|
+
|
|
16
|
+
def check(self, project_root, task_id: str) -> list[Gap]:
|
|
17
|
+
try:
|
|
18
|
+
status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
|
|
19
|
+
dirty_lines = [line for line in status.splitlines() if line.strip() and not self._is_runtime_state(line)]
|
|
20
|
+
if dirty_lines:
|
|
21
|
+
return [Gap(
|
|
22
|
+
id=f"GAP-{task_id}-DIRTY-GIT",
|
|
23
|
+
severity="high",
|
|
24
|
+
gap_type="architecture_drift",
|
|
25
|
+
task_id=task_id,
|
|
26
|
+
description="Git working tree is dirty. Execution requires a clean state for checkpointing.",
|
|
27
|
+
recommended_fix="Commit or stash your current changes before running the task.",
|
|
28
|
+
blocking=True
|
|
29
|
+
)]
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
logger.error("Git is not installed or not in PATH.")
|
|
32
|
+
return [Gap(
|
|
33
|
+
id=f"GAP-{task_id}-NO-GIT",
|
|
34
|
+
severity="high",
|
|
35
|
+
gap_type="architecture_drift",
|
|
36
|
+
task_id=task_id,
|
|
37
|
+
description="Git is not available. Cannot verify working tree cleanliness.",
|
|
38
|
+
recommended_fix="Install git and ensure it is in your PATH.",
|
|
39
|
+
blocking=True
|
|
40
|
+
)]
|
|
41
|
+
except subprocess.CalledProcessError as e:
|
|
42
|
+
logger.warning("Git status check failed: %s", e)
|
|
43
|
+
return [Gap(
|
|
44
|
+
id=f"GAP-{task_id}-GIT-ERROR",
|
|
45
|
+
severity="medium",
|
|
46
|
+
gap_type="architecture_drift",
|
|
47
|
+
task_id=task_id,
|
|
48
|
+
description=f"Git status check failed: {e}. Directory may not be a git repository.",
|
|
49
|
+
recommended_fix="Initialize a git repository with 'git init' before running tasks.",
|
|
50
|
+
blocking=True
|
|
51
|
+
)]
|
|
52
|
+
return []
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
from devcouncil.domain.task import Task
|
|
2
|
-
from devcouncil.domain.gap import Gap
|
|
3
|
-
|
|
4
|
-
class PlannedFilesCheck:
|
|
5
|
-
"""Ensures a task has legitimate files planned for modification."""
|
|
6
|
-
|
|
7
|
-
def check(self, task: Task) -> list[Gap]:
|
|
8
|
-
gaps = []
|
|
9
|
-
if not task.planned_files:
|
|
10
|
-
gaps.append(Gap(
|
|
11
|
-
id=f"GAP-{task.id}-NO-FILES",
|
|
12
|
-
severity="high",
|
|
13
|
-
gap_type="task_not_implemented",
|
|
14
|
-
task_id=task.id,
|
|
15
|
-
description=f"Task {task.id} has no planned files. Agents won't know where to write code.",
|
|
16
|
-
recommended_fix="Update the task to include at least one planned file path.",
|
|
17
|
-
blocking=True
|
|
18
|
-
))
|
|
19
|
-
|
|
20
|
-
has_modify = any(pf.allowed_change in ["create", "modify", "delete"] for pf in task.planned_files)
|
|
21
|
-
if task.planned_files and not has_modify:
|
|
22
|
-
gaps.append(Gap(
|
|
23
|
-
id=f"GAP-{task.id}-READ-ONLY",
|
|
24
|
-
severity="medium",
|
|
25
|
-
gap_type="task_not_implemented",
|
|
26
|
-
task_id=task.id,
|
|
27
|
-
description=f"Task {task.id} only has read-only files. No changes can be made.",
|
|
28
|
-
recommended_fix="Grant 'modify' or 'create' permissions to at least one file.",
|
|
29
|
-
blocking=True
|
|
30
|
-
))
|
|
31
|
-
|
|
32
|
-
return gaps
|
|
1
|
+
from devcouncil.domain.task import Task
|
|
2
|
+
from devcouncil.domain.gap import Gap
|
|
3
|
+
|
|
4
|
+
class PlannedFilesCheck:
|
|
5
|
+
"""Ensures a task has legitimate files planned for modification."""
|
|
6
|
+
|
|
7
|
+
def check(self, task: Task) -> list[Gap]:
|
|
8
|
+
gaps = []
|
|
9
|
+
if not task.planned_files:
|
|
10
|
+
gaps.append(Gap(
|
|
11
|
+
id=f"GAP-{task.id}-NO-FILES",
|
|
12
|
+
severity="high",
|
|
13
|
+
gap_type="task_not_implemented",
|
|
14
|
+
task_id=task.id,
|
|
15
|
+
description=f"Task {task.id} has no planned files. Agents won't know where to write code.",
|
|
16
|
+
recommended_fix="Update the task to include at least one planned file path.",
|
|
17
|
+
blocking=True
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
has_modify = any(pf.allowed_change in ["create", "modify", "delete"] for pf in task.planned_files)
|
|
21
|
+
if task.planned_files and not has_modify:
|
|
22
|
+
gaps.append(Gap(
|
|
23
|
+
id=f"GAP-{task.id}-READ-ONLY",
|
|
24
|
+
severity="medium",
|
|
25
|
+
gap_type="task_not_implemented",
|
|
26
|
+
task_id=task.id,
|
|
27
|
+
description=f"Task {task.id} only has read-only files. No changes can be made.",
|
|
28
|
+
recommended_fix="Grant 'modify' or 'create' permissions to at least one file.",
|
|
29
|
+
blocking=True
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
return gaps
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
from devcouncil.domain.requirement import Requirement
|
|
2
|
-
from devcouncil.domain.task import Task
|
|
3
|
-
from devcouncil.domain.gap import Gap
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
|
-
class RequirementCoverageCheck:
|
|
7
|
-
"""Detects requirements that are not mapped to any tasks."""
|
|
8
|
-
|
|
9
|
-
def check(self, requirements: List[Requirement], tasks: List[Task]) -> List[Gap]:
|
|
10
|
-
task_req_ids = set()
|
|
11
|
-
for t in tasks:
|
|
12
|
-
task_req_ids.update(t.requirement_ids)
|
|
13
|
-
|
|
14
|
-
gaps = []
|
|
15
|
-
for req in requirements:
|
|
16
|
-
if req.id not in task_req_ids:
|
|
17
|
-
gaps.append(Gap(
|
|
18
|
-
id=f"GAP-PLAN-{req.id}-UNMAPPED",
|
|
19
|
-
severity="high",
|
|
20
|
-
gap_type="requirement_not_planned",
|
|
21
|
-
requirement_id=req.id,
|
|
22
|
-
description=f"Requirement '{req.title}' is not covered by any task.",
|
|
23
|
-
recommended_fix="Decompose this requirement into one or more implementation tasks.",
|
|
24
|
-
blocking=True
|
|
25
|
-
))
|
|
26
|
-
return gaps
|
|
1
|
+
from devcouncil.domain.requirement import Requirement
|
|
2
|
+
from devcouncil.domain.task import Task
|
|
3
|
+
from devcouncil.domain.gap import Gap
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
class RequirementCoverageCheck:
|
|
7
|
+
"""Detects requirements that are not mapped to any tasks."""
|
|
8
|
+
|
|
9
|
+
def check(self, requirements: List[Requirement], tasks: List[Task]) -> List[Gap]:
|
|
10
|
+
task_req_ids = set()
|
|
11
|
+
for t in tasks:
|
|
12
|
+
task_req_ids.update(t.requirement_ids)
|
|
13
|
+
|
|
14
|
+
gaps = []
|
|
15
|
+
for req in requirements:
|
|
16
|
+
if req.id not in task_req_ids:
|
|
17
|
+
gaps.append(Gap(
|
|
18
|
+
id=f"GAP-PLAN-{req.id}-UNMAPPED",
|
|
19
|
+
severity="high",
|
|
20
|
+
gap_type="requirement_not_planned",
|
|
21
|
+
requirement_id=req.id,
|
|
22
|
+
description=f"Requirement '{req.title}' is not covered by any task.",
|
|
23
|
+
recommended_fix="Decompose this requirement into one or more implementation tasks.",
|
|
24
|
+
blocking=True
|
|
25
|
+
))
|
|
26
|
+
return gaps
|
|
@@ -1,34 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
from devcouncil.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
from devcouncil.domain.gap import Gap
|
|
4
|
+
from devcouncil.utils.redaction import SECRET_PATTERNS, redact_string
|
|
5
|
+
|
|
6
|
+
# Captures the new-file starting line from a unified-diff hunk header (@@ -a,b +c,d @@).
|
|
7
|
+
_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SecretScanner:
|
|
11
|
+
"""Scans code diffs for potential secrets (API keys, tokens, etc.)."""
|
|
12
|
+
|
|
13
|
+
def scan_diff(self, diff_content: str, task_id: str) -> List[Gap]:
|
|
14
|
+
gaps: List[Gap] = []
|
|
15
|
+
current_file = "unknown_file"
|
|
16
|
+
new_line_no = 0 # line number in the new file, tracked across hunks
|
|
17
|
+
counter = 0 # ensures unique gap ids
|
|
18
|
+
|
|
19
|
+
for line in diff_content.splitlines():
|
|
20
|
+
if line.startswith("+++ b/"):
|
|
21
|
+
current_file = line[6:]
|
|
22
|
+
continue
|
|
23
|
+
if line.startswith("+++") or line.startswith("---") or line.startswith("diff "):
|
|
24
|
+
continue
|
|
25
|
+
hunk = _HUNK_RE.match(line)
|
|
26
|
+
if hunk:
|
|
27
|
+
new_line_no = int(hunk.group(1))
|
|
28
|
+
continue
|
|
29
|
+
if line.startswith("-"):
|
|
30
|
+
continue # removed line — does not advance the new-file counter
|
|
31
|
+
if line.startswith("+"):
|
|
32
|
+
for key_type, pattern in SECRET_PATTERNS.items():
|
|
33
|
+
if pattern.search(line):
|
|
34
|
+
counter += 1
|
|
35
|
+
gaps.append(Gap(
|
|
36
|
+
id=f"GAP-{task_id}-SECRET-{key_type.upper()}-{new_line_no}-{counter}",
|
|
37
|
+
severity="critical",
|
|
38
|
+
gap_type="security_risk",
|
|
39
|
+
task_id=task_id,
|
|
40
|
+
description=f"Potential {key_type} found in {current_file}:{new_line_no}.",
|
|
41
|
+
evidence=[redact_string(line.strip())],
|
|
42
|
+
recommended_fix="Remove the secret and use environment variables or a secret manager.",
|
|
43
|
+
blocking=True,
|
|
44
|
+
# Populate the routing fields so the security NextAction points
|
|
45
|
+
# the agent straight at the file:line instead of forcing a re-grep.
|
|
46
|
+
file=current_file,
|
|
47
|
+
line=new_line_no,
|
|
48
|
+
))
|
|
49
|
+
new_line_no += 1
|
|
50
|
+
continue
|
|
51
|
+
# Context or blank line — advances the new-file counter.
|
|
52
|
+
new_line_no += 1
|
|
53
|
+
return gaps
|