@danmoisan/drm-copilot-mcp 0.0.6 → 1.0.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/out/mcp-server.js +10213 -398
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +127 -17
- package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +0 -12
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
- package/resources/config/orchestration-routing.json +3 -3
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +0 -5
- package/resources/scripts/dev_tools/__init__.py +0 -0
- package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +0 -127
- package/resources/scripts/dev_tools/_orchestrator_state_routing.py +0 -216
- package/resources/scripts/dev_tools/agentic_sync.py +0 -819
- package/resources/scripts/dev_tools/codex_native_converter/__init__.py +0 -11
- package/resources/scripts/dev_tools/codex_native_converter/__main__.py +0 -6
- package/resources/scripts/dev_tools/codex_native_converter/cli.py +0 -11
- package/resources/scripts/dev_tools/new_active_feature_folder.py +0 -79
- package/resources/scripts/dev_tools/new_active_feature_folder_docs.py +0 -268
- package/resources/scripts/dev_tools/new_active_feature_folder_flow.py +0 -366
- package/resources/scripts/dev_tools/new_active_feature_folder_io.py +0 -306
- package/resources/scripts/dev_tools/new_active_feature_folder_markdown.py +0 -252
- package/resources/scripts/dev_tools/new_active_feature_folder_models.py +0 -136
- package/resources/scripts/dev_tools/new_potential_bug_entry.py +0 -465
- package/resources/scripts/dev_tools/potential_to_issue.py +0 -421
- package/resources/scripts/dev_tools/potential_to_issue_content.py +0 -212
- package/resources/scripts/dev_tools/pr_context/__init__.py +0 -0
- package/resources/scripts/dev_tools/pr_context/collector.py +0 -619
- package/resources/scripts/dev_tools/pr_context/feature_docs.py +0 -349
- package/resources/scripts/dev_tools/pr_context/git.py +0 -153
- package/resources/scripts/dev_tools/pr_context/github.py +0 -549
- package/resources/scripts/dev_tools/pr_context/models.py +0 -198
- package/resources/scripts/dev_tools/pr_context/render.py +0 -342
- package/resources/scripts/dev_tools/pr_context/render_feature_excerpts.py +0 -256
- package/resources/scripts/dev_tools/pr_context/render_pr_helpers.py +0 -291
- package/resources/scripts/dev_tools/pr_context/summary_helpers.py +0 -386
- package/resources/scripts/dev_tools/pr_context/verification_evidence.py +0 -171
- package/resources/scripts/dev_tools/prompt_mode_contract.py +0 -152
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +0 -403
- package/resources/scripts/dev_tools/push_down_claude_filesystem.py +0 -472
- package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +0 -401
- package/resources/scripts/dev_tools/push_down_codex_and_agents_customizations.py +0 -139
- package/resources/scripts/dev_tools/push_down_copilot_customizations.py +0 -504
- package/resources/scripts/dev_tools/push_down_copilot_customizations_filesystem.py +0 -217
- package/resources/scripts/dev_tools/push_down_copilot_customizations_rewrites.py +0 -293
- package/resources/scripts/dev_tools/resolve_file_prompt.py +0 -457
- package/resources/scripts/dev_tools/resolve_hard_lock_prompt.py +0 -444
- package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +0 -246
- package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +0 -107
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +0 -505
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -472
- package/resources/templates/codex_native_converter.py +0 -35
- package/resources/templates/collect_commit_context.py +0 -212
- package/resources/templates/collect_pr_context.py +0 -74
- package/resources/templates/hello_python.py +0 -11
- package/resources/templates/new_active_feature_folder.py +0 -67
- package/resources/templates/new_potential_bug_entry.py +0 -54
- package/resources/templates/potential_to_issue.py +0 -55
- package/resources/templates/push_down_claude_customizations.py +0 -233
- package/resources/templates/push_down_codex_and_agents_customizations.py +0 -95
- package/resources/templates/push_down_copilot_customizations.py +0 -124
- package/resources/templates/resolve_atomic_plan_prompt.py +0 -75
- package/resources/templates/resolve_hard_lock_prompt.py +0 -65
- package/resources/templates/validate_orchestration_artifacts.py +0 -55
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
"""Collect Git repository context for pull request authorship."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
|
|
9
|
-
from .feature_docs import (
|
|
10
|
-
completed_plan_tasks,
|
|
11
|
-
extract_issue_references,
|
|
12
|
-
gather_feature_excerpts,
|
|
13
|
-
)
|
|
14
|
-
from .git import CommandRunner, GitClient, SubprocessRunner
|
|
15
|
-
from .github import GhClient
|
|
16
|
-
from .models import (
|
|
17
|
-
CommandResult,
|
|
18
|
-
FeatureDocExcerpt,
|
|
19
|
-
IssueDetails,
|
|
20
|
-
PRContextResult,
|
|
21
|
-
PullRequestDetails,
|
|
22
|
-
find_user_story_link,
|
|
23
|
-
format_list,
|
|
24
|
-
section,
|
|
25
|
-
truncate_lines,
|
|
26
|
-
)
|
|
27
|
-
from .render import (
|
|
28
|
-
build_close_candidates_section,
|
|
29
|
-
build_pr_context,
|
|
30
|
-
convert_numstat,
|
|
31
|
-
extension_summary,
|
|
32
|
-
extract_changed_paths,
|
|
33
|
-
extract_merge_pr_numbers,
|
|
34
|
-
format_diff_path,
|
|
35
|
-
format_issue_details,
|
|
36
|
-
format_pr_details,
|
|
37
|
-
select_default_base,
|
|
38
|
-
)
|
|
39
|
-
from .render_pr_helpers import build_issues_to_autoclose_section
|
|
40
|
-
from .summary_helpers import (
|
|
41
|
-
append_generation_timestamp,
|
|
42
|
-
bucket_text,
|
|
43
|
-
)
|
|
44
|
-
from .summary_helpers import (
|
|
45
|
-
issue_appendix as _issue_appendix,
|
|
46
|
-
)
|
|
47
|
-
from .summary_helpers import (
|
|
48
|
-
issue_digest as _issue_digest,
|
|
49
|
-
)
|
|
50
|
-
from .summary_helpers import (
|
|
51
|
-
parse_name_status_map as _parse_name_status_map,
|
|
52
|
-
)
|
|
53
|
-
from .summary_helpers import (
|
|
54
|
-
parse_numstat_detailed as _parse_numstat_detailed,
|
|
55
|
-
)
|
|
56
|
-
from .summary_helpers import (
|
|
57
|
-
pr_appendix as _pr_appendix,
|
|
58
|
-
)
|
|
59
|
-
from .summary_helpers import (
|
|
60
|
-
pr_digest as _pr_digest,
|
|
61
|
-
)
|
|
62
|
-
from .summary_helpers import (
|
|
63
|
-
scoping_doc_changes as _scoping_doc_changes,
|
|
64
|
-
)
|
|
65
|
-
from .verification_evidence import (
|
|
66
|
-
VerificationEvidenceRecord,
|
|
67
|
-
parse_verification_evidence_file,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if TYPE_CHECKING:
|
|
71
|
-
from collections.abc import Sequence
|
|
72
|
-
|
|
73
|
-
__all__ = [
|
|
74
|
-
"CommandResult",
|
|
75
|
-
"FeatureDocExcerpt",
|
|
76
|
-
"IssueDetails",
|
|
77
|
-
"PRContextResult",
|
|
78
|
-
"PullRequestDetails",
|
|
79
|
-
"GitClient",
|
|
80
|
-
"SubprocessRunner",
|
|
81
|
-
"CommandRunner",
|
|
82
|
-
"GhClient",
|
|
83
|
-
"build_pr_context",
|
|
84
|
-
"build_close_candidates_section",
|
|
85
|
-
"completed_plan_tasks",
|
|
86
|
-
"convert_numstat",
|
|
87
|
-
"extension_summary",
|
|
88
|
-
"extract_issue_references",
|
|
89
|
-
"extract_merge_pr_numbers",
|
|
90
|
-
"format_diff_path",
|
|
91
|
-
"format_issue_details",
|
|
92
|
-
"format_pr_details",
|
|
93
|
-
"gather_feature_excerpts",
|
|
94
|
-
"select_default_base",
|
|
95
|
-
"find_user_story_link",
|
|
96
|
-
"section",
|
|
97
|
-
"extract_changed_paths",
|
|
98
|
-
"collect_and_write",
|
|
99
|
-
"parse_args",
|
|
100
|
-
"main",
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
SUMMARY_PATH_DEFAULT = "artifacts/pr_context.summary.txt"
|
|
104
|
-
APPENDIX_PATH_DEFAULT = "artifacts/pr_context.appendix.txt"
|
|
105
|
-
SUMMARY_CHAR_BUDGET = 160000 # 10x increase: ~2300 lines at 70 chars/line avg
|
|
106
|
-
APPENDIX_CHAR_BUDGET = 480000 # 10x increase: ~6900 lines at 70 chars/line avg
|
|
107
|
-
ISSUE_SUMMARY_LINE_BUDGET = 25
|
|
108
|
-
ISSUE_APPENDIX_LINE_BUDGET = 120
|
|
109
|
-
COMMENT_SUMMARY_LIMIT = 3
|
|
110
|
-
COMMENT_APPENDIX_LIMIT = 10
|
|
111
|
-
PR_BODY_SUMMARY_LINES = 25
|
|
112
|
-
PR_BODY_APPENDIX_LINES = 120
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _render_verification_evidence_section(
|
|
116
|
-
*, resolved_root: Path, feature_docs: list[FeatureDocExcerpt]
|
|
117
|
-
) -> str:
|
|
118
|
-
"""Render canonical verification evidence rows for summary output.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
resolved_root: Repository root used to read discovered evidence files.
|
|
122
|
-
feature_docs: Feature excerpts whose context files may include evidence paths.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
A formatted section body containing parsed evidence rows or fallback text.
|
|
126
|
-
|
|
127
|
-
Side Effects:
|
|
128
|
-
Reads evidence files from disk and tolerates unreadable artifacts.
|
|
129
|
-
"""
|
|
130
|
-
records: list[VerificationEvidenceRecord] = []
|
|
131
|
-
# Parse only canonical evidence files already enumerated in context files.
|
|
132
|
-
for doc in feature_docs:
|
|
133
|
-
for raw_path in doc.context_files:
|
|
134
|
-
normalized = raw_path.replace("\\", "/")
|
|
135
|
-
if "/evidence/" not in normalized:
|
|
136
|
-
continue
|
|
137
|
-
try:
|
|
138
|
-
record: VerificationEvidenceRecord = parse_verification_evidence_file(
|
|
139
|
-
root=resolved_root,
|
|
140
|
-
feature=doc.feature,
|
|
141
|
-
relative_path=Path(normalized),
|
|
142
|
-
)
|
|
143
|
-
except OSError:
|
|
144
|
-
continue
|
|
145
|
-
records.append(record)
|
|
146
|
-
|
|
147
|
-
parseable_records = [
|
|
148
|
-
item for item in records if item.normalized_result in {"pass", "fail"}
|
|
149
|
-
]
|
|
150
|
-
if not parseable_records:
|
|
151
|
-
return "No canonical verification evidence parsed"
|
|
152
|
-
|
|
153
|
-
lines: list[str] = []
|
|
154
|
-
# Render deterministic rows sorted by source path for stable artifacts.
|
|
155
|
-
for record in sorted(parseable_records, key=lambda item: item.source_file):
|
|
156
|
-
lines.extend(
|
|
157
|
-
[
|
|
158
|
-
f"- Feature: {record.feature}",
|
|
159
|
-
f" - Source: {record.source_file}",
|
|
160
|
-
f" - Timestamp: {record.timestamp}",
|
|
161
|
-
f" - Command: {record.command}",
|
|
162
|
-
f" - EXIT_CODE: {record.exit_code}",
|
|
163
|
-
f" - Normalized result: {record.normalized_result}",
|
|
164
|
-
]
|
|
165
|
-
)
|
|
166
|
-
return "\n".join(lines)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# helper functions moved to summary_helpers
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def write_output(text: str, out_path: Path, append: bool) -> None:
|
|
173
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
-
mode = "a" if append else "w"
|
|
175
|
-
with out_path.open(mode, encoding="utf-8") as handle:
|
|
176
|
-
handle.write(text)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def collect_and_write(
|
|
180
|
-
*,
|
|
181
|
-
base: str | None,
|
|
182
|
-
head: str | None,
|
|
183
|
-
out: Path,
|
|
184
|
-
appendix_out: Path | None,
|
|
185
|
-
repo_root: Path,
|
|
186
|
-
append: bool,
|
|
187
|
-
include_untracked: bool,
|
|
188
|
-
) -> None:
|
|
189
|
-
runner = SubprocessRunner()
|
|
190
|
-
git = GitClient(runner, repo_root)
|
|
191
|
-
resolved_root = git.resolve_root()
|
|
192
|
-
git = GitClient(runner, resolved_root)
|
|
193
|
-
gh = GhClient(runner, resolved_root)
|
|
194
|
-
gh_available = True
|
|
195
|
-
gh_status_override: str | None = None
|
|
196
|
-
try:
|
|
197
|
-
gh.ensure_available()
|
|
198
|
-
except RuntimeError as exc: # pragma: no cover - availability gate
|
|
199
|
-
gh_available = False
|
|
200
|
-
gh_status_override = f"GitHub CLI unavailable: {exc}"
|
|
201
|
-
|
|
202
|
-
summary_path = out
|
|
203
|
-
appendix_path = appendix_out or Path(APPENDIX_PATH_DEFAULT)
|
|
204
|
-
|
|
205
|
-
current_pr = gh.current_pr()
|
|
206
|
-
|
|
207
|
-
context_result = build_pr_context(
|
|
208
|
-
git=git,
|
|
209
|
-
gh=gh,
|
|
210
|
-
base_ref=base,
|
|
211
|
-
head_ref=head,
|
|
212
|
-
include_untracked=include_untracked,
|
|
213
|
-
feature_issue_refs=[],
|
|
214
|
-
current_pr=current_pr,
|
|
215
|
-
gh_available=gh_available,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
changed_paths = extract_changed_paths(context_result.text)
|
|
219
|
-
feature_docs = gather_feature_excerpts(resolved_root, changed_paths)
|
|
220
|
-
additional_context_files = sorted(
|
|
221
|
-
{path for doc in feature_docs for path in doc.context_files if path}
|
|
222
|
-
)
|
|
223
|
-
feature_issue_refs = sorted(
|
|
224
|
-
{ref for doc in feature_docs for ref in doc.issue_refs if ref.strip()}
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
if feature_issue_refs:
|
|
228
|
-
context_result = build_pr_context(
|
|
229
|
-
git=git,
|
|
230
|
-
gh=gh,
|
|
231
|
-
base_ref=base,
|
|
232
|
-
head_ref=head,
|
|
233
|
-
include_untracked=include_untracked,
|
|
234
|
-
feature_issue_refs=feature_issue_refs,
|
|
235
|
-
current_pr=current_pr,
|
|
236
|
-
gh_available=gh_available,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
referenced_issues_set = set(context_result.referenced_issues)
|
|
240
|
-
referenced_prs_set = set(context_result.referenced_prs)
|
|
241
|
-
invalid_refs_set = set(context_result.invalid_references)
|
|
242
|
-
branch_refs = extract_issue_references(git.branch_name())
|
|
243
|
-
path_refs = extract_issue_references("\n".join(changed_paths))
|
|
244
|
-
if gh_available:
|
|
245
|
-
for ref in feature_issue_refs:
|
|
246
|
-
formatted = ref if ref.startswith("#") else f"#{ref}"
|
|
247
|
-
entity = gh.classify_entity(ref.lstrip("#"))
|
|
248
|
-
if entity == "issue":
|
|
249
|
-
referenced_issues_set.add(formatted)
|
|
250
|
-
elif entity == "pull":
|
|
251
|
-
referenced_prs_set.add(formatted)
|
|
252
|
-
else:
|
|
253
|
-
invalid_refs_set.add(formatted)
|
|
254
|
-
for ref in branch_refs + path_refs:
|
|
255
|
-
formatted = ref if ref.startswith("#") else f"#{ref}"
|
|
256
|
-
entity = gh.classify_entity(ref.lstrip("#"))
|
|
257
|
-
if entity == "issue":
|
|
258
|
-
referenced_issues_set.add(formatted)
|
|
259
|
-
elif entity == "pull":
|
|
260
|
-
referenced_prs_set.add(formatted)
|
|
261
|
-
else:
|
|
262
|
-
invalid_refs_set.add(formatted)
|
|
263
|
-
else:
|
|
264
|
-
referenced_issues_set.update(
|
|
265
|
-
formatted if formatted.startswith("#") else f"#{formatted}"
|
|
266
|
-
for formatted in feature_issue_refs
|
|
267
|
-
)
|
|
268
|
-
referenced_issues_set.update(
|
|
269
|
-
formatted if formatted.startswith("#") else f"#{formatted}"
|
|
270
|
-
for formatted in branch_refs + path_refs
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
referenced_issues = sorted(referenced_issues_set)
|
|
274
|
-
referenced_prs = sorted(referenced_prs_set)
|
|
275
|
-
invalid_refs = sorted(invalid_refs_set)
|
|
276
|
-
|
|
277
|
-
author_asserted: list[str] = []
|
|
278
|
-
author_reason = "None (author has not asserted autoclose issues)"
|
|
279
|
-
verified = context_result.verified_closing if gh_available else []
|
|
280
|
-
if not gh_available:
|
|
281
|
-
verified_reason = "None (GitHub CLI unavailable)"
|
|
282
|
-
elif current_pr is None:
|
|
283
|
-
verified_reason = "None (no PR exists yet for this branch)"
|
|
284
|
-
elif not verified:
|
|
285
|
-
verified_reason = "None (closingIssuesReferences empty)"
|
|
286
|
-
else:
|
|
287
|
-
verified_reason = "(verified from GitHub PR metadata)"
|
|
288
|
-
|
|
289
|
-
if referenced_issues:
|
|
290
|
-
author_asserted = sorted(set(author_asserted + referenced_issues))
|
|
291
|
-
author_reason = "Detected issue references (classified)"
|
|
292
|
-
|
|
293
|
-
# Derive deterministic pending autoclose targets from explicit metadata only
|
|
294
|
-
# when feature readiness is PASS.
|
|
295
|
-
pending_primary: list[str] = []
|
|
296
|
-
for feature_doc in feature_docs:
|
|
297
|
-
if feature_doc.readiness_signal != "PASS":
|
|
298
|
-
continue
|
|
299
|
-
if not feature_doc.primary_issue_ref:
|
|
300
|
-
continue
|
|
301
|
-
if feature_doc.primary_issue_ref not in pending_primary:
|
|
302
|
-
pending_primary.append(feature_doc.primary_issue_ref)
|
|
303
|
-
readiness_signals = sorted(
|
|
304
|
-
{
|
|
305
|
-
feature_doc.readiness_signal
|
|
306
|
-
for feature_doc in feature_docs
|
|
307
|
-
if feature_doc.readiness_signal
|
|
308
|
-
}
|
|
309
|
-
)
|
|
310
|
-
issues_to_autoclose_section = build_issues_to_autoclose_section(
|
|
311
|
-
verified=verified,
|
|
312
|
-
pending_primary=pending_primary,
|
|
313
|
-
readiness_signals=readiness_signals,
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
issues_to_fetch = sorted(set(verified + author_asserted + referenced_issues))
|
|
317
|
-
issue_details: list[IssueDetails] = []
|
|
318
|
-
if gh_available:
|
|
319
|
-
for ref in issues_to_fetch:
|
|
320
|
-
issue_details.append(gh.issue_details(ref.lstrip("#")))
|
|
321
|
-
|
|
322
|
-
pr_details_list: list[PullRequestDetails] = []
|
|
323
|
-
if gh_available:
|
|
324
|
-
for ref in referenced_prs:
|
|
325
|
-
pr_details_list.append(gh.pr_details(ref.lstrip("#")))
|
|
326
|
-
|
|
327
|
-
if context_result.merge_base and context_result.head_sha:
|
|
328
|
-
name_status_text = git.diff_range(
|
|
329
|
-
["--name-status", context_result.merge_base, context_result.head_sha]
|
|
330
|
-
)
|
|
331
|
-
numstat_text = git.diff_range(
|
|
332
|
-
["--numstat", context_result.merge_base, context_result.head_sha]
|
|
333
|
-
)
|
|
334
|
-
else:
|
|
335
|
-
name_status_text = git.diff_range(["--name-status"])
|
|
336
|
-
numstat_text = git.diff_range(["--numstat"])
|
|
337
|
-
|
|
338
|
-
_additions, _deletions, per_file_stats = _parse_numstat_detailed(numstat_text)
|
|
339
|
-
status_map = _parse_name_status_map(name_status_text)
|
|
340
|
-
|
|
341
|
-
scoping_changes = _scoping_doc_changes(
|
|
342
|
-
git=git,
|
|
343
|
-
merge_base=context_result.merge_base,
|
|
344
|
-
head_sha=context_result.head_sha,
|
|
345
|
-
root=resolved_root,
|
|
346
|
-
name_status_text=name_status_text,
|
|
347
|
-
numstat_details=per_file_stats,
|
|
348
|
-
)
|
|
349
|
-
material_scoping = [
|
|
350
|
-
(path, reasons, excerpt)
|
|
351
|
-
for path, material, reasons, excerpt in scoping_changes
|
|
352
|
-
if material
|
|
353
|
-
]
|
|
354
|
-
non_material_scoping = [
|
|
355
|
-
(path, reasons)
|
|
356
|
-
for path, material, reasons, _ in scoping_changes
|
|
357
|
-
if not material
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
ci_target = context_result.head_sha
|
|
361
|
-
if not ci_target:
|
|
362
|
-
try:
|
|
363
|
-
ci_target = git.rev_parse("HEAD")
|
|
364
|
-
except RuntimeError:
|
|
365
|
-
ci_target = None
|
|
366
|
-
ci_status, ci_jobs = (
|
|
367
|
-
gh.ci_status(ci_target) if (ci_target and gh_available) else (None, [])
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
bucket_core: list[tuple[str, tuple[int, int]]] = []
|
|
371
|
-
bucket_renames: list[tuple[str, tuple[int, int]]] = []
|
|
372
|
-
bucket_docs: list[tuple[str, tuple[int, int]]] = []
|
|
373
|
-
for path, status in status_map.items():
|
|
374
|
-
stats = per_file_stats.get(path, (0, 0))
|
|
375
|
-
if status.startswith("R"):
|
|
376
|
-
bucket_renames.append((path, stats))
|
|
377
|
-
elif path.endswith(".py") or path.endswith(".ps1"):
|
|
378
|
-
bucket_core.append((path, stats))
|
|
379
|
-
elif path.startswith("docs/") or path.startswith(".github") or "AGENTS" in path:
|
|
380
|
-
bucket_docs.append((path, stats))
|
|
381
|
-
|
|
382
|
-
scoping_summary_lines: list[str] = []
|
|
383
|
-
if material_scoping:
|
|
384
|
-
scoping_summary_lines.append("Scoping docs changed (material):")
|
|
385
|
-
for path, reasons, excerpt in material_scoping:
|
|
386
|
-
reason_text = (
|
|
387
|
-
f"Reasons: {', '.join(reasons) if reasons else '(unspecified)'}"
|
|
388
|
-
)
|
|
389
|
-
scoping_summary_lines.append(f"- {path} ({reason_text})")
|
|
390
|
-
if excerpt:
|
|
391
|
-
scoping_summary_lines.append(truncate_lines(excerpt, 40))
|
|
392
|
-
if non_material_scoping:
|
|
393
|
-
scoping_summary_lines.append("Scoping docs changed (non-material):")
|
|
394
|
-
for path, reasons in non_material_scoping[:5]:
|
|
395
|
-
reason_text = (
|
|
396
|
-
f"Reasons: {', '.join(reasons) if reasons else '(unspecified)'}"
|
|
397
|
-
)
|
|
398
|
-
scoping_summary_lines.append(f"- {path} ({reason_text})")
|
|
399
|
-
if not scoping_summary_lines:
|
|
400
|
-
scoping_summary_lines.append("(none)")
|
|
401
|
-
|
|
402
|
-
issue_digests = "\n\n".join(_issue_digest(detail) for detail in issue_details)
|
|
403
|
-
pr_digests = "\n\n".join(_pr_digest(detail) for detail in pr_details_list)
|
|
404
|
-
|
|
405
|
-
feature_summary_lines: list[str] = []
|
|
406
|
-
for doc in feature_docs:
|
|
407
|
-
feature_summary_lines.extend(
|
|
408
|
-
[
|
|
409
|
-
f"Feature: {doc.feature}",
|
|
410
|
-
"Excerpt:",
|
|
411
|
-
truncate_lines(doc.excerpt, 80),
|
|
412
|
-
"Context files:",
|
|
413
|
-
format_list(doc.context_files, "(none)"),
|
|
414
|
-
"",
|
|
415
|
-
]
|
|
416
|
-
)
|
|
417
|
-
feature_summary = (
|
|
418
|
-
"\n".join(feature_summary_lines).rstrip() if feature_summary_lines else "(none)"
|
|
419
|
-
)
|
|
420
|
-
verification_evidence_section = _render_verification_evidence_section(
|
|
421
|
-
resolved_root=resolved_root,
|
|
422
|
-
feature_docs=feature_docs,
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
close_candidates = build_close_candidates_section(
|
|
426
|
-
verified=verified,
|
|
427
|
-
author_asserted=sorted(set(author_asserted)),
|
|
428
|
-
referenced=referenced_issues,
|
|
429
|
-
verified_reason=verified_reason,
|
|
430
|
-
author_reason=author_reason,
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
gh_status_text = (
|
|
434
|
-
gh_status_override or gh.status_message or "GitHub CLI authenticated."
|
|
435
|
-
)
|
|
436
|
-
if not gh_available and not gh_status_override:
|
|
437
|
-
gh_status_text = "GitHub CLI unavailable; references unverified."
|
|
438
|
-
intent_block = "\n".join(
|
|
439
|
-
[
|
|
440
|
-
section("PR Intent"),
|
|
441
|
-
"Primary outcome:",
|
|
442
|
-
"User/dev impact:",
|
|
443
|
-
"Risks:",
|
|
444
|
-
"Author-asserted autoclose issues:",
|
|
445
|
-
]
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
summary_sections = [
|
|
449
|
-
section("GitHub CLI status"),
|
|
450
|
-
gh_status_text,
|
|
451
|
-
intent_block,
|
|
452
|
-
section("Base/Head"),
|
|
453
|
-
f"Base ref (requested): {context_result.base_ref or '(default)'}",
|
|
454
|
-
(
|
|
455
|
-
f"Base ref (resolved): {context_result.resolved_base or '(unknown)'} @ "
|
|
456
|
-
f"{context_result.base_sha or '(unknown)'}"
|
|
457
|
-
),
|
|
458
|
-
(
|
|
459
|
-
f"Head ref (resolved): {context_result.head_ref or head or '(unknown)'} @ "
|
|
460
|
-
f"{context_result.head_sha or '(unknown)'}"
|
|
461
|
-
),
|
|
462
|
-
f"Merge base: {context_result.merge_base or '(unknown)'}",
|
|
463
|
-
f"Range: {context_result.rev_range or '(unknown)'}",
|
|
464
|
-
]
|
|
465
|
-
if (
|
|
466
|
-
context_result.base_ref
|
|
467
|
-
and context_result.resolved_base
|
|
468
|
-
and not str(context_result.resolved_base).startswith("origin/")
|
|
469
|
-
):
|
|
470
|
-
summary_sections.append(
|
|
471
|
-
"WARNING: Requested base is local and may be stale; prefer "
|
|
472
|
-
f"origin/{context_result.base_ref}"
|
|
473
|
-
)
|
|
474
|
-
summary_sections.extend(
|
|
475
|
-
[
|
|
476
|
-
"",
|
|
477
|
-
issues_to_autoclose_section,
|
|
478
|
-
"",
|
|
479
|
-
close_candidates,
|
|
480
|
-
"",
|
|
481
|
-
section("Additional context files"),
|
|
482
|
-
format_list(additional_context_files, "(none)"),
|
|
483
|
-
"",
|
|
484
|
-
section("Feature doc excerpts"),
|
|
485
|
-
feature_summary,
|
|
486
|
-
"",
|
|
487
|
-
section("Referenced issues (classified)"),
|
|
488
|
-
format_list(referenced_issues, "(none)")
|
|
489
|
-
+ ("\nNOTE: Unverified (GitHub unavailable)" if not gh_available else ""),
|
|
490
|
-
"",
|
|
491
|
-
section("PRs in range (classified)"),
|
|
492
|
-
format_list(referenced_prs, "(none)"),
|
|
493
|
-
"",
|
|
494
|
-
section("Invalid references (not found)"),
|
|
495
|
-
format_list(invalid_refs, "(none)"),
|
|
496
|
-
"",
|
|
497
|
-
section("Scoping docs changed"),
|
|
498
|
-
"\n".join(scoping_summary_lines),
|
|
499
|
-
"",
|
|
500
|
-
section("Changed files overview"),
|
|
501
|
-
bucket_text("Core logic changes", bucket_core),
|
|
502
|
-
"",
|
|
503
|
-
bucket_text("Mechanical moves/renames", bucket_renames),
|
|
504
|
-
"",
|
|
505
|
-
bucket_text("Docs/templates/agents/tooling", bucket_docs),
|
|
506
|
-
"",
|
|
507
|
-
section("Issue digests"),
|
|
508
|
-
issue_digests or "(none)",
|
|
509
|
-
"",
|
|
510
|
-
section("PR digests"),
|
|
511
|
-
pr_digests or "(none)",
|
|
512
|
-
"",
|
|
513
|
-
section("Verification evidence (feature docs + canonical artifacts)"),
|
|
514
|
-
verification_evidence_section,
|
|
515
|
-
"",
|
|
516
|
-
section("CI status (HEAD)"),
|
|
517
|
-
(
|
|
518
|
-
f"Status: {ci_status}\n"
|
|
519
|
-
+ (f"Failing jobs: {', '.join(ci_jobs)}" if ci_jobs else "")
|
|
520
|
-
if ci_status
|
|
521
|
-
else "(not available)"
|
|
522
|
-
),
|
|
523
|
-
"",
|
|
524
|
-
section("Appendix pointer"),
|
|
525
|
-
f"See {appendix_path}",
|
|
526
|
-
]
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
summary_text = "\n".join(summary_sections)
|
|
530
|
-
if len(summary_text) > SUMMARY_CHAR_BUDGET:
|
|
531
|
-
summary_text = (
|
|
532
|
-
summary_text[:SUMMARY_CHAR_BUDGET] + "\nTRUNCATED: summary budget exceeded"
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
feature_block = "\n".join(doc.excerpt for doc in feature_docs)
|
|
536
|
-
|
|
537
|
-
issue_sections = [_issue_appendix(detail) for detail in issue_details]
|
|
538
|
-
pr_sections = [_pr_appendix(detail) for detail in pr_details_list]
|
|
539
|
-
appendix_parts = [
|
|
540
|
-
append_generation_timestamp(),
|
|
541
|
-
context_result.text,
|
|
542
|
-
"",
|
|
543
|
-
section("Issue details"),
|
|
544
|
-
"\n\n".join(issue_sections) if issue_sections else "(none)",
|
|
545
|
-
"",
|
|
546
|
-
section("Contributing pull requests"),
|
|
547
|
-
"\n\n".join(pr_sections) if pr_sections else "(none)",
|
|
548
|
-
]
|
|
549
|
-
if feature_block:
|
|
550
|
-
appendix_parts.extend(["", feature_block])
|
|
551
|
-
appendix_text = "\n".join(appendix_parts)
|
|
552
|
-
if len(appendix_text) > APPENDIX_CHAR_BUDGET:
|
|
553
|
-
appendix_text = (
|
|
554
|
-
appendix_text[:APPENDIX_CHAR_BUDGET]
|
|
555
|
-
+ "\nTRUNCATED: appendix budget exceeded"
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
write_output(summary_text, summary_path, append)
|
|
559
|
-
write_output(appendix_text, appendix_path, append)
|
|
560
|
-
print(f"Wrote context summary to: {summary_path}")
|
|
561
|
-
print(f"Wrote context appendix to: {appendix_path}")
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
565
|
-
parser = argparse.ArgumentParser(description="Collect PR context for GitHub.")
|
|
566
|
-
parser.add_argument(
|
|
567
|
-
"--base", dest="base", help="Base ref (default: auto-detect origin/main)"
|
|
568
|
-
)
|
|
569
|
-
parser.add_argument("--head", dest="head", help="Head ref (default: HEAD)")
|
|
570
|
-
parser.add_argument(
|
|
571
|
-
"--out",
|
|
572
|
-
dest="out",
|
|
573
|
-
default=SUMMARY_PATH_DEFAULT,
|
|
574
|
-
help="Summary output file path",
|
|
575
|
-
)
|
|
576
|
-
parser.add_argument(
|
|
577
|
-
"--appendix-out",
|
|
578
|
-
dest="appendix_out",
|
|
579
|
-
default=APPENDIX_PATH_DEFAULT,
|
|
580
|
-
help="Appendix output file path",
|
|
581
|
-
)
|
|
582
|
-
parser.add_argument(
|
|
583
|
-
"--repo-root",
|
|
584
|
-
dest="repo_root",
|
|
585
|
-
default=".",
|
|
586
|
-
help="Repository root (defaults to current directory)",
|
|
587
|
-
)
|
|
588
|
-
parser.add_argument(
|
|
589
|
-
"--append",
|
|
590
|
-
dest="append",
|
|
591
|
-
action="store_true",
|
|
592
|
-
help="Append instead of overwrite",
|
|
593
|
-
)
|
|
594
|
-
parser.add_argument(
|
|
595
|
-
"--no-untracked",
|
|
596
|
-
dest="no_untracked",
|
|
597
|
-
action="store_true",
|
|
598
|
-
help="Exclude untracked files from status",
|
|
599
|
-
)
|
|
600
|
-
return parser.parse_args(argv)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
def main(argv: Sequence[str] | None = None) -> None:
|
|
604
|
-
args = parse_args(argv)
|
|
605
|
-
out_path = Path(args.out).expanduser()
|
|
606
|
-
repo_root = Path(args.repo_root).expanduser().resolve()
|
|
607
|
-
collect_and_write(
|
|
608
|
-
base=args.base,
|
|
609
|
-
head=args.head,
|
|
610
|
-
out=out_path,
|
|
611
|
-
appendix_out=Path(args.appendix_out).expanduser(),
|
|
612
|
-
repo_root=repo_root,
|
|
613
|
-
append=bool(args.append),
|
|
614
|
-
include_untracked=not bool(args.no_untracked),
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if __name__ == "__main__":
|
|
619
|
-
main()
|