@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,444 +0,0 @@
|
|
|
1
|
-
"""Resolve the execute-hard-lock prompt with the plan file path.
|
|
2
|
-
|
|
3
|
-
Purpose:
|
|
4
|
-
This helper resolves the active plan file path from the target file,
|
|
5
|
-
substitutes the ${plan-path} variable into the execute-hard-lock.prompt.md
|
|
6
|
-
template, prints the result, and attempts to copy it to the clipboard for
|
|
7
|
-
pasting into Copilot Chat.
|
|
8
|
-
|
|
9
|
-
Supported variables:
|
|
10
|
-
- ${plan-path}: Workspace-relative path to the target plan file (forward
|
|
11
|
-
slashes).
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
python resolve_hard_lock_prompt.py --target <path_to_plan_file>
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import argparse
|
|
20
|
-
import shutil
|
|
21
|
-
import subprocess
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
from dev_tools.prompt_mode_contract import (
|
|
26
|
-
build_fallback_reason,
|
|
27
|
-
resolve_selected_work_mode,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _resolve_template_name(template_kind: str) -> str:
|
|
32
|
-
"""Map the CLI template-kind flag to a concrete template filename.
|
|
33
|
-
|
|
34
|
-
Purpose:
|
|
35
|
-
Keeps filename selection centralized so both root and bundled callers
|
|
36
|
-
resolve the same prompt asset names for `execute` and `resume` flows.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
template_kind: CLI selector constrained to `execute` or `resume`.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
str: The prompt filename associated with the requested template kind.
|
|
43
|
-
|
|
44
|
-
Side Effects:
|
|
45
|
-
None.
|
|
46
|
-
"""
|
|
47
|
-
return (
|
|
48
|
-
"execute-hard-lock.prompt.md"
|
|
49
|
-
if template_kind == "execute"
|
|
50
|
-
else "resume-hard-lock.prompt.md"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _resolve_template_path(
|
|
55
|
-
template_name: str,
|
|
56
|
-
workspace_root: Path,
|
|
57
|
-
template_root: Path | None,
|
|
58
|
-
) -> tuple[Path | None, tuple[Path, ...]]:
|
|
59
|
-
"""Resolve the first available prompt template path in deterministic order.
|
|
60
|
-
|
|
61
|
-
Purpose:
|
|
62
|
-
Supports bundled extension resources by checking an explicit template
|
|
63
|
-
root first while preserving the existing workspace `.github/codex`
|
|
64
|
-
fallback for repo-root usage.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
template_name: Prompt filename to resolve.
|
|
68
|
-
workspace_root: Workspace root used for repo-local fallback lookup.
|
|
69
|
-
template_root: Optional explicit template directory to probe first.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
tuple[Path | None, tuple[Path, ...]]: The selected template path when
|
|
73
|
-
found, plus the ordered list of checked candidate paths.
|
|
74
|
-
|
|
75
|
-
Side Effects:
|
|
76
|
-
None.
|
|
77
|
-
"""
|
|
78
|
-
candidates: list[Path] = []
|
|
79
|
-
if template_root is not None:
|
|
80
|
-
candidates.append(template_root / template_name)
|
|
81
|
-
candidates.append(workspace_root / ".github" / "codex" / template_name)
|
|
82
|
-
|
|
83
|
-
# Probe the explicit template root before the workspace fallback so bundled
|
|
84
|
-
# extension resources stay authoritative when they are intentionally passed.
|
|
85
|
-
for candidate in candidates:
|
|
86
|
-
if candidate.exists():
|
|
87
|
-
return candidate, tuple(candidates)
|
|
88
|
-
|
|
89
|
-
return None, tuple(candidates)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def copy_to_clipboard(text: str) -> bool:
|
|
93
|
-
"""Attempt to copy text to the clipboard using common tools.
|
|
94
|
-
|
|
95
|
-
Purpose:
|
|
96
|
-
Provides cross-platform clipboard support using either pyperclip
|
|
97
|
-
(if installed) or native system clipboard commands.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
text: The text to copy to the clipboard.
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
True on success, False if no supported clipboard mechanism is found.
|
|
104
|
-
|
|
105
|
-
Side Effects:
|
|
106
|
-
Writes to the system clipboard.
|
|
107
|
-
Prints error to stderr if pyperclip fails.
|
|
108
|
-
"""
|
|
109
|
-
try:
|
|
110
|
-
import pyperclip # type: ignore[import-untyped]
|
|
111
|
-
except ImportError:
|
|
112
|
-
pyperclip = None # type: ignore[assignment]
|
|
113
|
-
|
|
114
|
-
pyperclip_error: Exception | None = None
|
|
115
|
-
if pyperclip is not None:
|
|
116
|
-
try:
|
|
117
|
-
pyperclip.copy(text)
|
|
118
|
-
return True
|
|
119
|
-
except Exception as error: # noqa: BLE001 - CLI top-level error handling
|
|
120
|
-
pyperclip_error = error
|
|
121
|
-
|
|
122
|
-
# Fallback chain: try common clipboard commands across platforms
|
|
123
|
-
commands: tuple[list[str], ...] = (
|
|
124
|
-
["pbcopy"],
|
|
125
|
-
["wl-copy"],
|
|
126
|
-
["xclip", "-selection", "clipboard"],
|
|
127
|
-
["xsel", "--clipboard", "--input"],
|
|
128
|
-
["clip"],
|
|
129
|
-
["clip.exe"],
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
for command in commands:
|
|
133
|
-
executable = shutil.which(command[0])
|
|
134
|
-
if executable is None:
|
|
135
|
-
continue
|
|
136
|
-
try:
|
|
137
|
-
subprocess.run( # noqa: S603 - static analysis can't verify runtime validation
|
|
138
|
-
[executable, *command[1:]],
|
|
139
|
-
input=text,
|
|
140
|
-
text=True,
|
|
141
|
-
check=True,
|
|
142
|
-
)
|
|
143
|
-
return True
|
|
144
|
-
except subprocess.CalledProcessError:
|
|
145
|
-
continue
|
|
146
|
-
|
|
147
|
-
if pyperclip_error is not None:
|
|
148
|
-
print(f"pyperclip copy failed: {pyperclip_error}", file=sys.stderr)
|
|
149
|
-
|
|
150
|
-
return False
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _try_relative_to_workspace(path: Path, workspace_root: Path) -> Path:
|
|
154
|
-
"""Return path relative to workspace_root when possible.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
path: The path to make relative.
|
|
158
|
-
workspace_root: The workspace root directory.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
The relative path if possible, otherwise the original path.
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
return path.resolve().relative_to(workspace_root.resolve())
|
|
165
|
-
except ValueError:
|
|
166
|
-
return path
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _write_resolved_prompt(
|
|
170
|
-
resolved_prompt: str,
|
|
171
|
-
output_path: Path,
|
|
172
|
-
workspace_root: Path,
|
|
173
|
-
) -> Path:
|
|
174
|
-
"""Persist the resolved prompt text to a UTF-8 file, creating parents.
|
|
175
|
-
|
|
176
|
-
Purpose:
|
|
177
|
-
Provide a deterministic file-write side channel for non-interactive MCP
|
|
178
|
-
callers that cannot capture stdout from the wrapping ``executeScript``
|
|
179
|
-
invocation. Mirrors ``--target`` resolution: absolute paths are used
|
|
180
|
-
verbatim; relative paths resolve against ``workspace_root``.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
resolved_prompt (str): Fully resolved prompt text to persist verbatim.
|
|
184
|
-
output_path (Path): Path supplied via ``--output`` (absolute or
|
|
185
|
-
workspace-relative).
|
|
186
|
-
workspace_root (Path): Active workspace root used to resolve relative
|
|
187
|
-
output paths against the same base used for ``--target``.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
Path: Absolute path that was actually written.
|
|
191
|
-
|
|
192
|
-
Raises:
|
|
193
|
-
OSError: When creating the parent directory or writing the file fails.
|
|
194
|
-
|
|
195
|
-
Side Effects:
|
|
196
|
-
Creates missing parent directories and writes the file as UTF-8 text
|
|
197
|
-
with no BOM.
|
|
198
|
-
"""
|
|
199
|
-
# Absolute paths are honored verbatim; relative paths resolve against the
|
|
200
|
-
# workspace root to match how `--target` is resolved.
|
|
201
|
-
resolved_output = (
|
|
202
|
-
output_path if output_path.is_absolute() else workspace_root / output_path
|
|
203
|
-
)
|
|
204
|
-
# Create any missing parents so MCP callers can target deep directory paths
|
|
205
|
-
# without pre-staging. `exist_ok=True` keeps repeated writes idempotent.
|
|
206
|
-
resolved_output.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
-
resolved_output.write_text(resolved_prompt, encoding="utf-8")
|
|
208
|
-
return resolved_output
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _resolve_issue_file_for_target(target_path: Path, workspace_root: Path) -> Path:
|
|
212
|
-
"""Resolve the most likely issue.md path for a target plan file.
|
|
213
|
-
|
|
214
|
-
Purpose:
|
|
215
|
-
Hard-lock prompts are usually generated from a plan file located in a
|
|
216
|
-
feature folder (or a version subfolder like `v2`). This helper selects
|
|
217
|
-
the nearest deterministic `issue.md` candidate.
|
|
218
|
-
|
|
219
|
-
Args:
|
|
220
|
-
target_path: Path to the target plan file.
|
|
221
|
-
workspace_root: Workspace root for normalization.
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
Path: Candidate issue.md path to parse for work-mode marker.
|
|
225
|
-
"""
|
|
226
|
-
relative_target = _try_relative_to_workspace(target_path, workspace_root)
|
|
227
|
-
plan_dir = relative_target.parent
|
|
228
|
-
direct_issue = workspace_root / plan_dir / "issue.md"
|
|
229
|
-
if direct_issue.exists():
|
|
230
|
-
return direct_issue
|
|
231
|
-
|
|
232
|
-
if plan_dir.name.startswith("v") and len(plan_dir.parts) >= 2:
|
|
233
|
-
parent_issue = workspace_root / plan_dir.parent / "issue.md"
|
|
234
|
-
if parent_issue.exists():
|
|
235
|
-
return parent_issue
|
|
236
|
-
|
|
237
|
-
return direct_issue
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _resolve_work_mode_from_issue(
|
|
241
|
-
target_path: Path, workspace_root: Path
|
|
242
|
-
) -> tuple[str, str]:
|
|
243
|
-
"""Resolve selected work mode from issue.md with fail-closed behavior.
|
|
244
|
-
|
|
245
|
-
Purpose:
|
|
246
|
-
Enforce deterministic mode selection for prompt templates by reading the
|
|
247
|
-
persisted issue marker first and failing closed to `full-feature` on
|
|
248
|
-
missing or malformed data.
|
|
249
|
-
|
|
250
|
-
Args:
|
|
251
|
-
target_path: Target plan file path.
|
|
252
|
-
workspace_root: Workspace root for path resolution.
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
tuple[str, str]: Selected mode and fallback reason text.
|
|
256
|
-
"""
|
|
257
|
-
issue_path = _resolve_issue_file_for_target(target_path, workspace_root)
|
|
258
|
-
if not issue_path.exists():
|
|
259
|
-
return resolve_selected_work_mode(None), build_fallback_reason(None)
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
issue_content = issue_path.read_text(encoding="utf-8")
|
|
263
|
-
except OSError:
|
|
264
|
-
return (
|
|
265
|
-
resolve_selected_work_mode(None),
|
|
266
|
-
"issue.md unreadable; fail closed to full-feature",
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
return resolve_selected_work_mode(issue_content), build_fallback_reason(
|
|
270
|
-
issue_content
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def resolve_prompt(
|
|
275
|
-
template_content: str,
|
|
276
|
-
target_path: Path,
|
|
277
|
-
workspace_root: Path,
|
|
278
|
-
*,
|
|
279
|
-
work_mode: str | None = None,
|
|
280
|
-
fallback_reason: str | None = None,
|
|
281
|
-
) -> str:
|
|
282
|
-
"""Resolve ${plan-path} in the template with the target file path.
|
|
283
|
-
|
|
284
|
-
Purpose:
|
|
285
|
-
Substitutes the ${plan-path} variable with the workspace-relative path
|
|
286
|
-
to the target file, using forward slashes for consistency.
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
template_content: The raw template content containing ${plan-path}.
|
|
290
|
-
target_path: The path to the plan file.
|
|
291
|
-
workspace_root: The workspace root directory.
|
|
292
|
-
|
|
293
|
-
Returns:
|
|
294
|
-
The resolved template with ${plan-path} replaced.
|
|
295
|
-
|
|
296
|
-
Side Effects:
|
|
297
|
-
None. Pure function.
|
|
298
|
-
"""
|
|
299
|
-
relative_target = _try_relative_to_workspace(target_path, workspace_root)
|
|
300
|
-
plan_path_value = relative_target.as_posix()
|
|
301
|
-
|
|
302
|
-
selected_mode, resolved_fallback_reason = _resolve_work_mode_from_issue(
|
|
303
|
-
target_path,
|
|
304
|
-
workspace_root,
|
|
305
|
-
)
|
|
306
|
-
mode_value = work_mode if work_mode is not None else selected_mode
|
|
307
|
-
fallback_value = (
|
|
308
|
-
fallback_reason if fallback_reason is not None else resolved_fallback_reason
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
resolved = template_content.replace("${plan-path}", plan_path_value)
|
|
312
|
-
resolved = resolved.replace("${work-mode}", mode_value)
|
|
313
|
-
resolved = resolved.replace("${fallback-reason}", fallback_value)
|
|
314
|
-
|
|
315
|
-
return resolved
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def main() -> int:
|
|
319
|
-
"""Main CLI entry point.
|
|
320
|
-
|
|
321
|
-
Returns:
|
|
322
|
-
Exit code: 0 on success, 1 on error.
|
|
323
|
-
|
|
324
|
-
Side Effects:
|
|
325
|
-
Reads files, writes to clipboard, prints to stdout/stderr.
|
|
326
|
-
"""
|
|
327
|
-
parser = argparse.ArgumentParser(
|
|
328
|
-
description="Resolve execute-hard-lock prompt with plan file path"
|
|
329
|
-
)
|
|
330
|
-
parser.add_argument(
|
|
331
|
-
"--target",
|
|
332
|
-
type=Path,
|
|
333
|
-
required=True,
|
|
334
|
-
help="Path to the plan file",
|
|
335
|
-
)
|
|
336
|
-
parser.add_argument(
|
|
337
|
-
"--workspace",
|
|
338
|
-
type=Path,
|
|
339
|
-
default=None,
|
|
340
|
-
help="Workspace root directory (default: current working directory)",
|
|
341
|
-
)
|
|
342
|
-
parser.add_argument(
|
|
343
|
-
"--template-kind",
|
|
344
|
-
choices=("execute", "resume"),
|
|
345
|
-
default="execute",
|
|
346
|
-
help="Hard-lock template kind to resolve.",
|
|
347
|
-
)
|
|
348
|
-
parser.add_argument(
|
|
349
|
-
"--template-root",
|
|
350
|
-
type=Path,
|
|
351
|
-
default=None,
|
|
352
|
-
help="Optional directory containing hard-lock prompt templates.",
|
|
353
|
-
)
|
|
354
|
-
parser.add_argument(
|
|
355
|
-
"--output",
|
|
356
|
-
type=Path,
|
|
357
|
-
default=None,
|
|
358
|
-
help=(
|
|
359
|
-
"Optional path to write the resolved prompt text as UTF-8. "
|
|
360
|
-
"Relative paths resolve against --workspace (or cwd when omitted). "
|
|
361
|
-
"Parent directories are created if missing."
|
|
362
|
-
),
|
|
363
|
-
)
|
|
364
|
-
parser.add_argument(
|
|
365
|
-
"--quiet",
|
|
366
|
-
action="store_true",
|
|
367
|
-
default=False,
|
|
368
|
-
help=(
|
|
369
|
-
"Suppress stdout print and clipboard copy when --output is also "
|
|
370
|
-
"provided. Using --quiet without --output is a hard error because "
|
|
371
|
-
"it would produce no user-visible output."
|
|
372
|
-
),
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
args = parser.parse_args()
|
|
376
|
-
|
|
377
|
-
# --quiet without --output would silently drop the resolved prompt; reject
|
|
378
|
-
# the combination explicitly so misconfigured callers see the failure.
|
|
379
|
-
if args.quiet and args.output is None:
|
|
380
|
-
print(
|
|
381
|
-
"Error: --quiet requires --output; --quiet alone would suppress all "
|
|
382
|
-
"output.",
|
|
383
|
-
file=sys.stderr,
|
|
384
|
-
)
|
|
385
|
-
return 1
|
|
386
|
-
|
|
387
|
-
workspace_root = args.workspace if args.workspace else Path.cwd()
|
|
388
|
-
template_name = _resolve_template_name(args.template_kind)
|
|
389
|
-
template_path, checked_paths = _resolve_template_path(
|
|
390
|
-
template_name,
|
|
391
|
-
workspace_root,
|
|
392
|
-
args.template_root,
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
if template_path is None:
|
|
396
|
-
checked_paths_text = "\n".join(f"- {candidate}" for candidate in checked_paths)
|
|
397
|
-
print(
|
|
398
|
-
"Error: Template "
|
|
399
|
-
f"'{template_name}' not found. Checked locations:\n{checked_paths_text}",
|
|
400
|
-
file=sys.stderr,
|
|
401
|
-
)
|
|
402
|
-
return 1
|
|
403
|
-
|
|
404
|
-
if not args.target.exists():
|
|
405
|
-
print(f"Error: Target file not found at {args.target}", file=sys.stderr)
|
|
406
|
-
return 1
|
|
407
|
-
|
|
408
|
-
try:
|
|
409
|
-
template_content = template_path.read_text(encoding="utf-8")
|
|
410
|
-
except OSError as error:
|
|
411
|
-
print(f"Error reading template: {error}", file=sys.stderr)
|
|
412
|
-
return 1
|
|
413
|
-
|
|
414
|
-
resolved_prompt = resolve_prompt(template_content, args.target, workspace_root)
|
|
415
|
-
|
|
416
|
-
# When --output is provided, persist the resolved prompt so MCP callers
|
|
417
|
-
# that cannot capture stdout still receive the full content.
|
|
418
|
-
if args.output is not None:
|
|
419
|
-
try:
|
|
420
|
-
_write_resolved_prompt(resolved_prompt, args.output, workspace_root)
|
|
421
|
-
except OSError as error:
|
|
422
|
-
print(f"Error writing output file: {error}", file=sys.stderr)
|
|
423
|
-
return 1
|
|
424
|
-
|
|
425
|
-
# --quiet combined with --output suppresses both stdout and clipboard so
|
|
426
|
-
# non-interactive callers produce only the file side channel.
|
|
427
|
-
if args.quiet:
|
|
428
|
-
return 0
|
|
429
|
-
|
|
430
|
-
print(resolved_prompt)
|
|
431
|
-
|
|
432
|
-
if copy_to_clipboard(resolved_prompt):
|
|
433
|
-
print("\n✓ Copied to clipboard", file=sys.stderr)
|
|
434
|
-
else:
|
|
435
|
-
print(
|
|
436
|
-
"\n✗ Could not copy to clipboard (no supported mechanism found)",
|
|
437
|
-
file=sys.stderr,
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
return 0
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if __name__ == "__main__":
|
|
444
|
-
sys.exit(main())
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
"""Validate orchestration plan, review, and checkpoint artifacts.
|
|
2
|
-
|
|
3
|
-
Purpose:
|
|
4
|
-
Provide the stable CLI entrypoint for deterministic orchestration artifact
|
|
5
|
-
validation while delegating review- and checkpoint-specific logic to smaller
|
|
6
|
-
modules that remain under the repository file-size limit.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import argparse
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
from dev_tools.validate_orchestration_review_artifacts import (
|
|
17
|
-
validate_code_review_text,
|
|
18
|
-
validate_feature_audit_text,
|
|
19
|
-
)
|
|
20
|
-
from dev_tools.validate_orchestrator_state import (
|
|
21
|
-
validate_orchestrator_state_text,
|
|
22
|
-
)
|
|
23
|
-
from dev_tools.validate_policy_audit_artifact import validate_policy_audit_text
|
|
24
|
-
|
|
25
|
-
PLAN_PHASE_RE = re.compile(r"^### Phase (?P<phase>\d+) — (?P<title>.+)$")
|
|
26
|
-
PLAN_TASK_RE = re.compile(
|
|
27
|
-
r"^- \[(?P<state>[ xX])\] \[P(?P<phase>\d+)-T(?P<task>\d+)\] (?P<title>.+)$"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _read_text(path: Path) -> str:
|
|
32
|
-
"""Read a UTF-8 artifact from disk.
|
|
33
|
-
|
|
34
|
-
Purpose:
|
|
35
|
-
Centralize file reading for the CLI dispatcher and monkeypatched unit
|
|
36
|
-
tests.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
path (Path): Artifact path to read.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
str: UTF-8 text content for the requested artifact.
|
|
43
|
-
|
|
44
|
-
Raises:
|
|
45
|
-
OSError: Raised when the file cannot be read.
|
|
46
|
-
|
|
47
|
-
Side Effects:
|
|
48
|
-
Reads from disk.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
return path.read_text(encoding="utf-8")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def validate_plan_text(text: str) -> list[str]:
|
|
55
|
-
"""Validate canonical atomic-plan structure.
|
|
56
|
-
|
|
57
|
-
Purpose:
|
|
58
|
-
Enforce the repository's required phase and task formatting for atomic
|
|
59
|
-
execution plans.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
text (str): Full plan document text.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
list[str]: Validation errors describing any structural contract
|
|
66
|
-
violations.
|
|
67
|
-
|
|
68
|
-
Raises:
|
|
69
|
-
None.
|
|
70
|
-
|
|
71
|
-
Side Effects:
|
|
72
|
-
None.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
errors: list[str] = []
|
|
76
|
-
current_phase: int | None = None
|
|
77
|
-
seen_phases: list[int] = []
|
|
78
|
-
expected_task_num: dict[int, int] = {}
|
|
79
|
-
found_task = False
|
|
80
|
-
|
|
81
|
-
# Walk the document in source order so numbering and phase mismatches are
|
|
82
|
-
# reported against the same line numbers a maintainer sees in the plan.
|
|
83
|
-
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
84
|
-
if line.startswith("### Phase "):
|
|
85
|
-
match = PLAN_PHASE_RE.match(line)
|
|
86
|
-
if match is None:
|
|
87
|
-
errors.append(
|
|
88
|
-
f"Line {line_number}: phase heading must match "
|
|
89
|
-
"`### Phase N — <Title>`."
|
|
90
|
-
)
|
|
91
|
-
current_phase = None
|
|
92
|
-
continue
|
|
93
|
-
current_phase = int(match.group("phase"))
|
|
94
|
-
seen_phases.append(current_phase)
|
|
95
|
-
expected_task_num.setdefault(current_phase, 1)
|
|
96
|
-
continue
|
|
97
|
-
|
|
98
|
-
if line.startswith("- [") and "[P" in line and "-T" in line:
|
|
99
|
-
found_task = True
|
|
100
|
-
match = PLAN_TASK_RE.match(line)
|
|
101
|
-
if match is None:
|
|
102
|
-
errors.append(
|
|
103
|
-
f"Line {line_number}: task line must match "
|
|
104
|
-
"`- [ ] [P#-T#] <Title>`."
|
|
105
|
-
)
|
|
106
|
-
continue
|
|
107
|
-
task_phase = int(match.group("phase"))
|
|
108
|
-
task_num = int(match.group("task"))
|
|
109
|
-
if current_phase is None:
|
|
110
|
-
errors.append(
|
|
111
|
-
"Line "
|
|
112
|
-
f"{line_number}: task appears before a canonical phase "
|
|
113
|
-
"heading."
|
|
114
|
-
)
|
|
115
|
-
continue
|
|
116
|
-
if task_phase != current_phase:
|
|
117
|
-
errors.append(
|
|
118
|
-
f"Line {line_number}: task phase P{task_phase} does not match "
|
|
119
|
-
f"current phase {current_phase}."
|
|
120
|
-
)
|
|
121
|
-
expected = expected_task_num.setdefault(task_phase, 1)
|
|
122
|
-
if task_num != expected:
|
|
123
|
-
errors.append(
|
|
124
|
-
f"Line {line_number}: expected task number T{expected} for phase "
|
|
125
|
-
f"{task_phase}, found T{task_num}."
|
|
126
|
-
)
|
|
127
|
-
expected_task_num[task_phase] = max(expected, task_num) + 1
|
|
128
|
-
|
|
129
|
-
if not seen_phases:
|
|
130
|
-
errors.append("Plan does not contain any canonical phase headings.")
|
|
131
|
-
if not found_task:
|
|
132
|
-
errors.append("Plan does not contain any canonical task lines.")
|
|
133
|
-
return errors
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
137
|
-
"""Create the CLI parser.
|
|
138
|
-
|
|
139
|
-
Purpose:
|
|
140
|
-
Define the stable command-line contract for orchestration artifact
|
|
141
|
-
validation.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
None.
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
argparse.ArgumentParser: Configured parser for the supported artifact
|
|
148
|
-
types.
|
|
149
|
-
|
|
150
|
-
Raises:
|
|
151
|
-
None.
|
|
152
|
-
|
|
153
|
-
Side Effects:
|
|
154
|
-
None.
|
|
155
|
-
"""
|
|
156
|
-
|
|
157
|
-
parser = argparse.ArgumentParser(
|
|
158
|
-
description="Validate deterministic orchestration artifacts."
|
|
159
|
-
)
|
|
160
|
-
subparsers = parser.add_subparsers(dest="artifact_type", required=True)
|
|
161
|
-
|
|
162
|
-
for artifact_type in ("plan", "policy-audit", "code-review", "feature-audit"):
|
|
163
|
-
artifact_parser = subparsers.add_parser(artifact_type)
|
|
164
|
-
artifact_parser.add_argument("path")
|
|
165
|
-
|
|
166
|
-
state_parser = subparsers.add_parser("orchestrator-state")
|
|
167
|
-
state_parser.add_argument("path")
|
|
168
|
-
state_parser.add_argument(
|
|
169
|
-
"--require-complete",
|
|
170
|
-
action="store_true",
|
|
171
|
-
help="Require all tracked statuses to be complete-state safe.",
|
|
172
|
-
)
|
|
173
|
-
return parser
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _validate_from_args(args: argparse.Namespace) -> list[str]:
|
|
177
|
-
"""Dispatch the requested validator.
|
|
178
|
-
|
|
179
|
-
Purpose:
|
|
180
|
-
Route the parsed CLI request to the correct validator without changing
|
|
181
|
-
the public artifact-type names accepted by the entrypoint.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
args (argparse.Namespace): Parsed CLI arguments.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
list[str]: Validation errors produced by the selected validator.
|
|
188
|
-
|
|
189
|
-
Raises:
|
|
190
|
-
None.
|
|
191
|
-
|
|
192
|
-
Side Effects:
|
|
193
|
-
Reads the target artifact from disk.
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
path = Path(args.path)
|
|
197
|
-
text = _read_text(path)
|
|
198
|
-
if args.artifact_type == "plan":
|
|
199
|
-
return validate_plan_text(text)
|
|
200
|
-
if args.artifact_type == "policy-audit":
|
|
201
|
-
return validate_policy_audit_text(text)
|
|
202
|
-
if args.artifact_type == "code-review":
|
|
203
|
-
return validate_code_review_text(text)
|
|
204
|
-
if args.artifact_type == "feature-audit":
|
|
205
|
-
return validate_feature_audit_text(text)
|
|
206
|
-
if args.artifact_type == "orchestrator-state":
|
|
207
|
-
return validate_orchestrator_state_text(
|
|
208
|
-
text, require_complete=bool(args.require_complete)
|
|
209
|
-
)
|
|
210
|
-
return [f"Unsupported artifact type: {args.artifact_type}"]
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def main(argv: list[str] | None = None) -> int:
|
|
214
|
-
"""Run the orchestration-artifact validator CLI.
|
|
215
|
-
|
|
216
|
-
Purpose:
|
|
217
|
-
Execute the stable CLI entrypoint that validates orchestration plans,
|
|
218
|
-
review artifacts, and checkpoint state.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
argv (list[str] | None): Optional command-line arguments for testing or
|
|
222
|
-
programmatic invocation.
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
int: Exit code `0` for success and `1` for validation failure.
|
|
226
|
-
|
|
227
|
-
Raises:
|
|
228
|
-
None.
|
|
229
|
-
|
|
230
|
-
Side Effects:
|
|
231
|
-
Reads files from disk and writes validation results to stdout/stderr.
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
parser = build_parser()
|
|
235
|
-
args = parser.parse_args(argv)
|
|
236
|
-
errors = _validate_from_args(args)
|
|
237
|
-
if errors:
|
|
238
|
-
for error in errors:
|
|
239
|
-
print(error, file=sys.stderr)
|
|
240
|
-
return 1
|
|
241
|
-
print(f"{args.artifact_type} validation passed: {args.path}")
|
|
242
|
-
return 0
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if __name__ == "__main__":
|
|
246
|
-
raise SystemExit(main())
|