claude-dev-env 1.17.2 → 1.19.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/bin/install.mjs +145 -63
- package/hooks/blocking/content-search-to-zoekt-redirector.py +55 -0
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +25 -0
- package/hooks/blocking/content_search_zoekt_block_payload.py +17 -0
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +24 -0
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +131 -0
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +19 -0
- package/hooks/blocking/destructive-command-blocker.py +53 -4
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +54 -0
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +51 -0
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +102 -0
- package/hooks/blocking/test_destructive_command_blocker.py +108 -0
- package/package.json +4 -1
- package/skills/rule-audit/SKILL.md +2 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +0 -64
- package/hooks/blocking/prompt_workflow_clipboard.py +0 -63
- package/hooks/blocking/prompt_workflow_gate_config.py +0 -113
- package/hooks/blocking/prompt_workflow_gate_core.py +0 -289
- package/hooks/blocking/prompt_workflow_validate.py +0 -218
- package/hooks/blocking/test_prompt_workflow_clipboard.py +0 -54
- package/hooks/blocking/test_prompt_workflow_gate_core.py +0 -195
- package/hooks/blocking/test_prompt_workflow_validate.py +0 -339
- package/rules/prompt-workflow-context-controls.md +0 -48
- package/skills/agent-prompt/SKILL.md +0 -199
- package/skills/prompt-generator/ARCHITECTURE.md +0 -18
- package/skills/prompt-generator/REFERENCE.md +0 -254
- package/skills/prompt-generator/REFINEMENT_PIPELINE_RUNBOOK.md +0 -177
- package/skills/prompt-generator/SKILL.md +0 -354
- package/skills/prompt-generator/TARGET_OUTPUT.md +0 -133
- package/skills/prompt-generator/evals/prompt-generator.json +0 -207
- package/skills/prompt-generator/templates/skill-from-ground-up.md +0 -104
- package/skills/prompt-generator/templates/skill-refinement-package.md +0 -109
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Shared deterministic checks for prompt workflow hooks."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import re
|
|
7
|
-
import textwrap
|
|
8
|
-
from typing import Iterable
|
|
9
|
-
|
|
10
|
-
from prompt_workflow_gate_config import (
|
|
11
|
-
AMBIGUOUS_SCOPE_TERMS,
|
|
12
|
-
COMPILED_NEGATIVE_INDIRECT_PATTERNS,
|
|
13
|
-
COMPILED_NEGATIVE_KEYWORD_PATTERNS,
|
|
14
|
-
DEBUG_INTENT_MARKERS,
|
|
15
|
-
INTERNAL_OBJECT_MARKERS,
|
|
16
|
-
PROMPT_WORKFLOW_RESPONSE_MARKERS,
|
|
17
|
-
REQUIRED_CHECKLIST_ROWS,
|
|
18
|
-
REQUIRED_SCOPE_ANCHORS,
|
|
19
|
-
REQUIRED_XML_SECTIONS,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
TRIPLE_BACKTICK = "```"
|
|
23
|
-
AUDIT_LINE_PATTERN = re.compile(r"^\s*[●•]?\s*(Audit:\s*.+?)\s*$")
|
|
24
|
-
|
|
25
|
-
def _line_opens_xml_fence(line: str) -> bool:
|
|
26
|
-
stripped = line.strip()
|
|
27
|
-
if not stripped.startswith(TRIPLE_BACKTICK):
|
|
28
|
-
return False
|
|
29
|
-
fence_marker_length = len(TRIPLE_BACKTICK)
|
|
30
|
-
remainder = stripped[fence_marker_length:].strip()
|
|
31
|
-
return remainder == "xml" or remainder.startswith("xml ")
|
|
32
|
-
|
|
33
|
-
def _line_is_bare_fence_close(line: str) -> bool:
|
|
34
|
-
return line.strip() == TRIPLE_BACKTICK
|
|
35
|
-
|
|
36
|
-
def _line_opens_inner_markdown_fence(line: str) -> bool:
|
|
37
|
-
stripped = line.strip()
|
|
38
|
-
if not stripped.startswith(TRIPLE_BACKTICK):
|
|
39
|
-
return False
|
|
40
|
-
return stripped != TRIPLE_BACKTICK
|
|
41
|
-
|
|
42
|
-
def _collect_inner_markdown_fence(
|
|
43
|
-
lines: list[str],
|
|
44
|
-
start_index: int,
|
|
45
|
-
) -> tuple[list[str], int]:
|
|
46
|
-
inner_lines: list[str] = []
|
|
47
|
-
index = start_index
|
|
48
|
-
while index < len(lines):
|
|
49
|
-
current_line = lines[index]
|
|
50
|
-
inner_lines.append(current_line)
|
|
51
|
-
index += 1
|
|
52
|
-
if _line_is_bare_fence_close(current_line):
|
|
53
|
-
break
|
|
54
|
-
return inner_lines, index
|
|
55
|
-
|
|
56
|
-
def _collect_xml_fence_body(
|
|
57
|
-
lines: list[str],
|
|
58
|
-
start_index: int,
|
|
59
|
-
) -> tuple[list[str], int]:
|
|
60
|
-
body_lines: list[str] = []
|
|
61
|
-
index = start_index
|
|
62
|
-
while index < len(lines):
|
|
63
|
-
current_line = lines[index]
|
|
64
|
-
if _line_is_bare_fence_close(current_line):
|
|
65
|
-
return body_lines, index + 1
|
|
66
|
-
if _line_opens_inner_markdown_fence(current_line):
|
|
67
|
-
inner_lines, index = _collect_inner_markdown_fence(lines, index)
|
|
68
|
-
body_lines.extend(inner_lines)
|
|
69
|
-
continue
|
|
70
|
-
body_lines.append(current_line)
|
|
71
|
-
index += 1
|
|
72
|
-
return body_lines, index
|
|
73
|
-
|
|
74
|
-
def extract_fenced_xml_content(text: str) -> str:
|
|
75
|
-
"""Extract bodies of ```xml fenced blocks.
|
|
76
|
-
|
|
77
|
-
The closing delimiter is a line whose stripped text is exactly three backticks.
|
|
78
|
-
Inner Markdown code fences (for example a line starting with three backticks
|
|
79
|
-
plus a language tag) are scanned until their own closing backtick line so the
|
|
80
|
-
outer ``xml`` fence does not end early.
|
|
81
|
-
"""
|
|
82
|
-
results: list[str] = []
|
|
83
|
-
lines = text.splitlines()
|
|
84
|
-
index = 0
|
|
85
|
-
while index < len(lines):
|
|
86
|
-
if not _line_opens_xml_fence(lines[index]):
|
|
87
|
-
index += 1
|
|
88
|
-
continue
|
|
89
|
-
body_lines, index = _collect_xml_fence_body(lines, index + 1)
|
|
90
|
-
results.append("\n".join(body_lines))
|
|
91
|
-
return "\n".join(results)
|
|
92
|
-
|
|
93
|
-
def _line_is_audit_line(line: str) -> bool:
|
|
94
|
-
return AUDIT_LINE_PATTERN.match(line) is not None
|
|
95
|
-
|
|
96
|
-
def _normalize_audit_line(line: str) -> str:
|
|
97
|
-
match = AUDIT_LINE_PATTERN.match(line)
|
|
98
|
-
if match:
|
|
99
|
-
return match.group(1).strip()
|
|
100
|
-
return line.strip()
|
|
101
|
-
|
|
102
|
-
def _line_starts_exported_artifact(line: str) -> bool:
|
|
103
|
-
stripped = line.strip()
|
|
104
|
-
if not stripped:
|
|
105
|
-
return False
|
|
106
|
-
if _line_opens_xml_fence(stripped):
|
|
107
|
-
return True
|
|
108
|
-
exported_artifact_pattern = re.compile(
|
|
109
|
-
r"^<(\?xml\b|prompt\b|runtime_context\b|role\b|background\b|instructions\b|constraints\b|output_format\b|illustrations\b|open_question\b)",
|
|
110
|
-
)
|
|
111
|
-
return exported_artifact_pattern.match(stripped) is not None
|
|
112
|
-
|
|
113
|
-
def _trim_trailing_blank_lines(lines: list[str]) -> list[str]:
|
|
114
|
-
trimmed = list(lines)
|
|
115
|
-
while trimmed and not trimmed[-1].strip():
|
|
116
|
-
trimmed.pop()
|
|
117
|
-
return trimmed
|
|
118
|
-
|
|
119
|
-
def _trim_flattened_export_tail(lines: list[str]) -> list[str]:
|
|
120
|
-
trimmed = _trim_trailing_blank_lines(lines)
|
|
121
|
-
while trimmed and trimmed[-1].lstrip().startswith("✻ "):
|
|
122
|
-
trimmed.pop()
|
|
123
|
-
trimmed = _trim_trailing_blank_lines(trimmed)
|
|
124
|
-
return trimmed
|
|
125
|
-
|
|
126
|
-
def _find_last_audit_index(lines: list[str]) -> int | None:
|
|
127
|
-
last_audit_index: int | None = None
|
|
128
|
-
for index, line in enumerate(lines):
|
|
129
|
-
if _line_is_audit_line(line):
|
|
130
|
-
last_audit_index = index
|
|
131
|
-
return last_audit_index
|
|
132
|
-
|
|
133
|
-
def _find_first_artifact_index(lines: list[str]) -> int | None:
|
|
134
|
-
for index, line in enumerate(lines):
|
|
135
|
-
if _line_starts_exported_artifact(line):
|
|
136
|
-
return index
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
def _rebuild_from_existing_fence(audit_line: str, artifact_text: str) -> str:
|
|
140
|
-
fenced_body = extract_fenced_xml_content(artifact_text).strip()
|
|
141
|
-
if not fenced_body:
|
|
142
|
-
return audit_line
|
|
143
|
-
return f"{audit_line}\n```xml\n{fenced_body}\n```"
|
|
144
|
-
|
|
145
|
-
def _rebuild_from_flattened_body(audit_line: str, artifact_text: str) -> str:
|
|
146
|
-
dedented_body = textwrap.dedent(artifact_text).strip("\n")
|
|
147
|
-
if not dedented_body:
|
|
148
|
-
return audit_line
|
|
149
|
-
return f"{audit_line}\n```xml\n{dedented_body}\n```"
|
|
150
|
-
|
|
151
|
-
def _rebuild_canonical_export(audit_line: str, artifact_lines: list[str]) -> str:
|
|
152
|
-
if not artifact_lines:
|
|
153
|
-
return audit_line
|
|
154
|
-
artifact_text = "\n".join(artifact_lines).rstrip()
|
|
155
|
-
if _line_opens_xml_fence(artifact_lines[0]):
|
|
156
|
-
return _rebuild_from_existing_fence(audit_line, artifact_text)
|
|
157
|
-
return _rebuild_from_flattened_body(audit_line, artifact_text)
|
|
158
|
-
|
|
159
|
-
def normalize_prompt_workflow_export(text: str) -> str:
|
|
160
|
-
"""Return the last successful Audit + fenced XML pair from a message or export.
|
|
161
|
-
|
|
162
|
-
Saved transcript exports can flatten blocked retry turns and strip the outer
|
|
163
|
-
``xml`` fence. This helper keeps only the last successful ``Audit:`` attempt
|
|
164
|
-
and rebuilds the canonical audit-plus-fence shape used by prompt-workflow
|
|
165
|
-
hooks and reviewers.
|
|
166
|
-
"""
|
|
167
|
-
lines = text.splitlines()
|
|
168
|
-
last_audit_index = _find_last_audit_index(lines)
|
|
169
|
-
if last_audit_index is None:
|
|
170
|
-
return text.strip()
|
|
171
|
-
audit_line = _normalize_audit_line(lines[last_audit_index])
|
|
172
|
-
artifact_index = _find_first_artifact_index(lines[last_audit_index + 1 :])
|
|
173
|
-
if artifact_index is None:
|
|
174
|
-
return audit_line
|
|
175
|
-
artifact_lines = _trim_flattened_export_tail(
|
|
176
|
-
lines[last_audit_index + 1 + artifact_index :],
|
|
177
|
-
)
|
|
178
|
-
return _rebuild_canonical_export(audit_line, artifact_lines)
|
|
179
|
-
|
|
180
|
-
def extract_fenced_xml_content_from_export(text: str) -> str:
|
|
181
|
-
"""Extract fenced XML from a canonical message or flattened transcript export."""
|
|
182
|
-
normalized = normalize_prompt_workflow_export(text)
|
|
183
|
-
return extract_fenced_xml_content(normalized)
|
|
184
|
-
|
|
185
|
-
def missing_required_xml_sections(text: str) -> list[str]:
|
|
186
|
-
fenced_body = extract_fenced_xml_content(text)
|
|
187
|
-
if not fenced_body.strip():
|
|
188
|
-
return []
|
|
189
|
-
missing_sections: list[str] = []
|
|
190
|
-
for section_name in REQUIRED_XML_SECTIONS:
|
|
191
|
-
open_tag = re.compile(rf"<{re.escape(section_name)}(\s[^>]*)?>")
|
|
192
|
-
close_tag = re.compile(rf"</{re.escape(section_name)}>")
|
|
193
|
-
if not open_tag.search(fenced_body) or not close_tag.search(fenced_body):
|
|
194
|
-
missing_sections.append(section_name)
|
|
195
|
-
return missing_sections
|
|
196
|
-
|
|
197
|
-
def _build_negative_keyword_violation(
|
|
198
|
-
match: re.Match[str],
|
|
199
|
-
line_number: int,
|
|
200
|
-
line_text: str,
|
|
201
|
-
) -> dict[str, str | int]:
|
|
202
|
-
return {
|
|
203
|
-
"keyword": match.group(),
|
|
204
|
-
"line_number": line_number,
|
|
205
|
-
"line_text": line_text.strip(),
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
def _find_pattern_violations(
|
|
209
|
-
patterns: Iterable[re.Pattern[str]],
|
|
210
|
-
line_text: str,
|
|
211
|
-
line_number: int,
|
|
212
|
-
) -> list[dict[str, str | int]]:
|
|
213
|
-
violations: list[dict[str, str | int]] = []
|
|
214
|
-
for pattern in patterns:
|
|
215
|
-
match = pattern.search(line_text)
|
|
216
|
-
if match:
|
|
217
|
-
violations.append(
|
|
218
|
-
_build_negative_keyword_violation(match, line_number, line_text),
|
|
219
|
-
)
|
|
220
|
-
return violations
|
|
221
|
-
|
|
222
|
-
def find_negative_keywords_in_fenced_xml(
|
|
223
|
-
text: str,
|
|
224
|
-
) -> list[dict[str, str | int]]:
|
|
225
|
-
fenced_content = extract_fenced_xml_content(text)
|
|
226
|
-
if not fenced_content:
|
|
227
|
-
return []
|
|
228
|
-
all_violations: list[dict[str, str | int]] = []
|
|
229
|
-
for line_index, each_line in enumerate(fenced_content.splitlines(), start=1):
|
|
230
|
-
all_violations.extend(
|
|
231
|
-
_find_pattern_violations(
|
|
232
|
-
COMPILED_NEGATIVE_KEYWORD_PATTERNS,
|
|
233
|
-
each_line,
|
|
234
|
-
line_index,
|
|
235
|
-
),
|
|
236
|
-
)
|
|
237
|
-
all_violations.extend(
|
|
238
|
-
_find_pattern_violations(
|
|
239
|
-
COMPILED_NEGATIVE_INDIRECT_PATTERNS,
|
|
240
|
-
each_line,
|
|
241
|
-
line_index,
|
|
242
|
-
),
|
|
243
|
-
)
|
|
244
|
-
return all_violations
|
|
245
|
-
|
|
246
|
-
def _contains_any_marker(text: str, markers: Iterable[str]) -> bool:
|
|
247
|
-
lower_text = text.lower()
|
|
248
|
-
return any(marker.lower() in lower_text for marker in markers)
|
|
249
|
-
|
|
250
|
-
def has_debug_intent(text: str) -> bool:
|
|
251
|
-
return _contains_any_marker(text, DEBUG_INTENT_MARKERS)
|
|
252
|
-
|
|
253
|
-
def has_internal_object_leak(text: str) -> bool:
|
|
254
|
-
return _contains_any_marker(text, INTERNAL_OBJECT_MARKERS)
|
|
255
|
-
|
|
256
|
-
def missing_scope_anchors(text: str) -> list[str]:
|
|
257
|
-
return [anchor for anchor in REQUIRED_SCOPE_ANCHORS if anchor not in text]
|
|
258
|
-
|
|
259
|
-
def find_ambiguous_scope_terms(text: str) -> list[str]:
|
|
260
|
-
if "scope" not in text.lower():
|
|
261
|
-
return []
|
|
262
|
-
matches: list[str] = []
|
|
263
|
-
lower_text = text.lower()
|
|
264
|
-
for term in AMBIGUOUS_SCOPE_TERMS:
|
|
265
|
-
if re.search(rf"\b{re.escape(term)}\b", lower_text):
|
|
266
|
-
matches.append(term)
|
|
267
|
-
return matches
|
|
268
|
-
|
|
269
|
-
def has_checklist_container(text: str) -> bool:
|
|
270
|
-
lower_text = text.lower()
|
|
271
|
-
return "checklist_results" in lower_text or "checklist:" in lower_text
|
|
272
|
-
|
|
273
|
-
def missing_checklist_rows(text: str) -> list[str]:
|
|
274
|
-
return [row for row in REQUIRED_CHECKLIST_ROWS if row not in text]
|
|
275
|
-
|
|
276
|
-
def is_prompt_workflow_response(text: str) -> bool:
|
|
277
|
-
lower_text = text.lower()
|
|
278
|
-
matched_markers = [
|
|
279
|
-
marker for marker in PROMPT_WORKFLOW_RESPONSE_MARKERS if marker in lower_text
|
|
280
|
-
]
|
|
281
|
-
return len(matched_markers) >= 2
|
|
282
|
-
|
|
283
|
-
def missing_context_control_signals(text: str) -> list[str]:
|
|
284
|
-
required_signals: tuple[str, ...] = (
|
|
285
|
-
"base_minimal_instruction_layer: true",
|
|
286
|
-
"on_demand_skill_loading: true",
|
|
287
|
-
)
|
|
288
|
-
lowered = text.lower()
|
|
289
|
-
return [signal for signal in required_signals if signal not in lowered]
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Shared prompt-workflow validator callable from tests, CLI, and the Stop hook.
|
|
3
|
-
|
|
4
|
-
Public API
|
|
5
|
-
----------
|
|
6
|
-
validate_prompt_workflow(assistant_message, user_context="")
|
|
7
|
-
Returns a ``ValidationResult`` with allowed/blocked status and reasons.
|
|
8
|
-
|
|
9
|
-
CLI
|
|
10
|
-
---
|
|
11
|
-
python prompt_workflow_validate.py path/to/draft.md
|
|
12
|
-
cat draft.md | python prompt_workflow_validate.py
|
|
13
|
-
|
|
14
|
-
Exit codes match hook conventions: 0 allowed, 2 blocked.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import sys
|
|
20
|
-
from dataclasses import dataclass, field
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
from prompt_workflow_gate_core import (
|
|
24
|
-
find_ambiguous_scope_terms,
|
|
25
|
-
find_negative_keywords_in_fenced_xml,
|
|
26
|
-
has_checklist_container,
|
|
27
|
-
has_debug_intent,
|
|
28
|
-
has_internal_object_leak,
|
|
29
|
-
is_prompt_workflow_response,
|
|
30
|
-
missing_checklist_rows,
|
|
31
|
-
missing_context_control_signals,
|
|
32
|
-
missing_required_xml_sections,
|
|
33
|
-
missing_scope_anchors,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
@dataclass(frozen=True)
|
|
37
|
-
class ValidationReason:
|
|
38
|
-
code: str
|
|
39
|
-
message: str
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass(frozen=True)
|
|
43
|
-
class ValidationResult:
|
|
44
|
-
allowed: bool
|
|
45
|
-
reasons: tuple[ValidationReason, ...] = field(default_factory=tuple)
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def reason_messages(self) -> list[str]:
|
|
49
|
-
return [each_reason.message for each_reason in self.reasons]
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def reason_codes(self) -> list[str]:
|
|
53
|
-
return [each_reason.code for each_reason in self.reasons]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _blocked(code: str, message: str) -> ValidationResult:
|
|
57
|
-
return ValidationResult(
|
|
58
|
-
allowed=False,
|
|
59
|
-
reasons=(ValidationReason(code=code, message=message),),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _check_internal_leak(
|
|
64
|
-
assistant_message: str,
|
|
65
|
-
debug_requested: bool,
|
|
66
|
-
) -> ValidationResult | None:
|
|
67
|
-
if not has_internal_object_leak(assistant_message) or debug_requested:
|
|
68
|
-
return None
|
|
69
|
-
return _blocked(
|
|
70
|
-
code="internal_object_leak",
|
|
71
|
-
message=(
|
|
72
|
-
"Raw internal refinement object leakage detected. "
|
|
73
|
-
"Return sanitized user-facing output unless explicit debug intent is present."
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _check_required_sections(assistant_message: str) -> ValidationResult | None:
|
|
79
|
-
missing_sections = missing_required_xml_sections(assistant_message)
|
|
80
|
-
if not missing_sections:
|
|
81
|
-
return None
|
|
82
|
-
return _blocked(
|
|
83
|
-
code="missing_xml_sections",
|
|
84
|
-
message=(
|
|
85
|
-
"Fenced XML artifact missing required sections: "
|
|
86
|
-
+ ", ".join(missing_sections)
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _check_checklist_rows(assistant_message: str) -> ValidationResult | None:
|
|
92
|
-
if not has_checklist_container(assistant_message):
|
|
93
|
-
return None
|
|
94
|
-
missing_rows = missing_checklist_rows(assistant_message)
|
|
95
|
-
if not missing_rows:
|
|
96
|
-
return None
|
|
97
|
-
return _blocked(
|
|
98
|
-
code="missing_checklist_rows",
|
|
99
|
-
message=("Deterministic checklist rows missing: " + ", ".join(missing_rows)),
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _check_scope_anchors(assistant_message: str) -> ValidationResult | None:
|
|
104
|
-
missing_anchors = missing_scope_anchors(assistant_message)
|
|
105
|
-
if not missing_anchors:
|
|
106
|
-
return None
|
|
107
|
-
return _blocked(
|
|
108
|
-
code="missing_scope_anchors",
|
|
109
|
-
message=("Required scope anchors missing: " + ", ".join(missing_anchors)),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _check_context_signals(assistant_message: str) -> ValidationResult | None:
|
|
114
|
-
missing_signals = missing_context_control_signals(assistant_message)
|
|
115
|
-
if not missing_signals:
|
|
116
|
-
return None
|
|
117
|
-
return _blocked(
|
|
118
|
-
code="missing_context_signals",
|
|
119
|
-
message=(
|
|
120
|
-
"Runtime context-control preamble missing. "
|
|
121
|
-
"Include the two required lines from prompt-workflow-context-controls "
|
|
122
|
-
"(minimal instruction layer and on-demand skill loading)."
|
|
123
|
-
),
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _check_ambiguous_scope(assistant_message: str) -> ValidationResult | None:
|
|
128
|
-
ambiguous_terms = find_ambiguous_scope_terms(assistant_message)
|
|
129
|
-
if not ambiguous_terms:
|
|
130
|
-
return None
|
|
131
|
-
return _blocked(
|
|
132
|
-
code="ambiguous_scope",
|
|
133
|
-
message=("Ambiguous scope phrasing detected: " + ", ".join(ambiguous_terms)),
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _check_negative_keywords(assistant_message: str) -> ValidationResult | None:
|
|
138
|
-
violations = find_negative_keywords_in_fenced_xml(assistant_message)
|
|
139
|
-
if not violations:
|
|
140
|
-
return None
|
|
141
|
-
violation_descriptions = [
|
|
142
|
-
f" line {each_violation['line_number']}: "
|
|
143
|
-
f'"{each_violation["keyword"]}" in: {each_violation["line_text"]}'
|
|
144
|
-
for each_violation in violations
|
|
145
|
-
]
|
|
146
|
-
return _blocked(
|
|
147
|
-
code="negative_keywords_in_artifact",
|
|
148
|
-
message=(
|
|
149
|
-
"Banned negative keywords found inside fenced XML artifact. "
|
|
150
|
-
"Rephrase as positive directives (what TO do, not what to avoid):\n"
|
|
151
|
-
+ "\n".join(violation_descriptions)
|
|
152
|
-
),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def validate_prompt_workflow(
|
|
157
|
-
assistant_message: str,
|
|
158
|
-
user_context: str = "",
|
|
159
|
-
) -> ValidationResult:
|
|
160
|
-
"""Run all prompt-workflow gates on *assistant_message*.
|
|
161
|
-
|
|
162
|
-
Returns ``ValidationResult.allowed == True`` when every gate passes.
|
|
163
|
-
The first failing gate short-circuits and its reason is returned.
|
|
164
|
-
"""
|
|
165
|
-
allowed_result = ValidationResult(allowed=True)
|
|
166
|
-
|
|
167
|
-
if not assistant_message.strip():
|
|
168
|
-
return allowed_result
|
|
169
|
-
|
|
170
|
-
debug_requested = has_debug_intent(user_context)
|
|
171
|
-
|
|
172
|
-
leak_result = _check_internal_leak(assistant_message, debug_requested)
|
|
173
|
-
if leak_result is not None:
|
|
174
|
-
return leak_result
|
|
175
|
-
|
|
176
|
-
if not is_prompt_workflow_response(assistant_message):
|
|
177
|
-
return allowed_result
|
|
178
|
-
|
|
179
|
-
workflow_checks = (
|
|
180
|
-
_check_required_sections,
|
|
181
|
-
_check_checklist_rows,
|
|
182
|
-
_check_scope_anchors,
|
|
183
|
-
_check_context_signals,
|
|
184
|
-
_check_ambiguous_scope,
|
|
185
|
-
_check_negative_keywords,
|
|
186
|
-
)
|
|
187
|
-
for each_check in workflow_checks:
|
|
188
|
-
gate_result = each_check(assistant_message)
|
|
189
|
-
if gate_result is not None:
|
|
190
|
-
return gate_result
|
|
191
|
-
|
|
192
|
-
return allowed_result
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def main() -> None:
|
|
196
|
-
blocked_exit_code: int = 2
|
|
197
|
-
allowed_exit_code: int = 0
|
|
198
|
-
|
|
199
|
-
if len(sys.argv) > 1:
|
|
200
|
-
file_path = Path(sys.argv[1])
|
|
201
|
-
assistant_text = file_path.read_text(encoding="utf-8")
|
|
202
|
-
elif not sys.stdin.isatty():
|
|
203
|
-
assistant_text = sys.stdin.read()
|
|
204
|
-
else:
|
|
205
|
-
sys.stderr.write("Usage: prompt_workflow_validate.py [path/to/draft.md]\n")
|
|
206
|
-
sys.stderr.write(" cat draft.md | prompt_workflow_validate.py\n")
|
|
207
|
-
sys.exit(blocked_exit_code)
|
|
208
|
-
|
|
209
|
-
validation_result = validate_prompt_workflow(assistant_text)
|
|
210
|
-
if validation_result.allowed:
|
|
211
|
-
sys.exit(allowed_exit_code)
|
|
212
|
-
for each_reason in validation_result.reasons:
|
|
213
|
-
sys.stderr.write(f"[{each_reason.code}] {each_reason.message}\n")
|
|
214
|
-
sys.exit(blocked_exit_code)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if __name__ == "__main__":
|
|
218
|
-
main()
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Tests for prompt_workflow_clipboard."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from prompt_workflow_clipboard import copy_text_to_system_clipboard
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_empty_text_returns_false() -> None:
|
|
11
|
-
assert copy_text_to_system_clipboard("") is False
|
|
12
|
-
assert copy_text_to_system_clipboard(" \n") is False
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_respects_skip_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
16
|
-
monkeypatch.setenv("PROMPT_WORKFLOW_SKIP_CLIPBOARD", "1")
|
|
17
|
-
assert copy_text_to_system_clipboard("hello") is False
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.mark.parametrize(
|
|
21
|
-
"flag_value",
|
|
22
|
-
("1", "true", "YES", "on"),
|
|
23
|
-
)
|
|
24
|
-
def test_skip_env_variants(monkeypatch: pytest.MonkeyPatch, flag_value: str) -> None:
|
|
25
|
-
monkeypatch.setenv("PROMPT_WORKFLOW_SKIP_CLIPBOARD", flag_value)
|
|
26
|
-
assert copy_text_to_system_clipboard("payload") is False
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_prefers_tkinter_when_it_succeeds(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
30
|
-
monkeypatch.delenv("PROMPT_WORKFLOW_SKIP_CLIPBOARD", raising=False)
|
|
31
|
-
monkeypatch.setattr(
|
|
32
|
-
"prompt_workflow_clipboard._copy_via_tkinter",
|
|
33
|
-
lambda t: True,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
def _fail_pyperclip(_t: str) -> bool:
|
|
37
|
-
raise AssertionError("pyperclip must run only when tkinter fails")
|
|
38
|
-
|
|
39
|
-
monkeypatch.setattr("prompt_workflow_clipboard._copy_via_pyperclip", _fail_pyperclip)
|
|
40
|
-
assert copy_text_to_system_clipboard("hello") is True
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_falls_back_to_pyperclip(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
44
|
-
monkeypatch.delenv("PROMPT_WORKFLOW_SKIP_CLIPBOARD", raising=False)
|
|
45
|
-
monkeypatch.setattr("prompt_workflow_clipboard._copy_via_tkinter", lambda t: False)
|
|
46
|
-
monkeypatch.setattr("prompt_workflow_clipboard._copy_via_pyperclip", lambda t: True)
|
|
47
|
-
assert copy_text_to_system_clipboard("hello") is True
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_returns_false_when_both_backends_fail(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
51
|
-
monkeypatch.delenv("PROMPT_WORKFLOW_SKIP_CLIPBOARD", raising=False)
|
|
52
|
-
monkeypatch.setattr("prompt_workflow_clipboard._copy_via_tkinter", lambda t: False)
|
|
53
|
-
monkeypatch.setattr("prompt_workflow_clipboard._copy_via_pyperclip", lambda t: False)
|
|
54
|
-
assert copy_text_to_system_clipboard("hello") is False
|