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.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. 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 check(self, project_root, task_id: str) -> list[Gap]:
11
- try:
12
- status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
13
- if status.strip():
14
- return [Gap(
15
- id=f"GAP-{task_id}-DIRTY-GIT",
16
- severity="high",
17
- gap_type="architecture_drift",
18
- task_id=task_id,
19
- description="Git working tree is dirty. Execution requires a clean state for checkpointing.",
20
- recommended_fix="Commit or stash your current changes before running the task.",
21
- blocking=True
22
- )]
23
- except FileNotFoundError:
24
- logger.error("Git is not installed or not in PATH.")
25
- return [Gap(
26
- id=f"GAP-{task_id}-NO-GIT",
27
- severity="high",
28
- gap_type="architecture_drift",
29
- task_id=task_id,
30
- description="Git is not available. Cannot verify working tree cleanliness.",
31
- recommended_fix="Install git and ensure it is in your PATH.",
32
- blocking=True
33
- )]
34
- except subprocess.CalledProcessError as e:
35
- logger.warning("Git status check failed: %s", e)
36
- return [Gap(
37
- id=f"GAP-{task_id}-GIT-ERROR",
38
- severity="medium",
39
- gap_type="architecture_drift",
40
- task_id=task_id,
41
- description=f"Git status check failed: {e}. Directory may not be a git repository.",
42
- recommended_fix="Initialize a git repository with 'git init' before running tasks.",
43
- blocking=True
44
- )]
45
- return []
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
- from typing import List
2
- from devcouncil.domain.gap import Gap
3
- from devcouncil.utils.redaction import SECRET_PATTERNS, redact_string
4
-
5
- class SecretScanner:
6
- """Scans code diffs for potential secrets (API keys, tokens, etc.)."""
7
-
8
- def scan_diff(self, diff_content: str, task_id: str) -> List[Gap]:
9
- gaps = []
10
- lines = diff_content.splitlines()
11
- current_file = "unknown_file"
12
-
13
- for i, line in enumerate(lines):
14
- if line.startswith("+++ b/"):
15
- current_file = line[6:]
16
- continue
17
-
18
- # Only scan added lines in diff
19
- if not line.startswith("+") or line.startswith("+++"):
20
- continue
21
-
22
- for key_type, pattern in SECRET_PATTERNS.items():
23
- if pattern.search(line):
24
- gaps.append(Gap(
25
- id=f"GAP-{task_id}-SECRET-{key_type.upper()}-{i}",
26
- severity="critical",
27
- gap_type="security_risk",
28
- task_id=task_id,
29
- description=f"Potential {key_type} found in {current_file} (diff line {i+1}).",
30
- evidence=[redact_string(line.strip())],
31
- recommended_fix="Remove the secret and use environment variables or a secret manager.",
32
- blocking=True
33
- ))
34
- return gaps
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