claude-dev-env 1.17.5 → 1.19.1
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/hooks/blocking/content-search-to-zoekt-redirector.py +6 -2
- package/hooks/blocking/content_search_zoekt_block_payload.py +9 -5
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +7 -1
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +8 -1
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +7 -2
- package/hooks/hooks.json +15 -0
- package/package.json +1 -1
- 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
|
@@ -8,7 +8,10 @@ import sys
|
|
|
8
8
|
from content_search_zoekt_bash_block_reason import block_reason_for_bash_command
|
|
9
9
|
from content_search_zoekt_block_payload import build_block_payload
|
|
10
10
|
from content_search_zoekt_indexed_paths import is_in_indexed_repo, is_specific_file
|
|
11
|
-
from content_search_zoekt_redirect_guidance import
|
|
11
|
+
from content_search_zoekt_redirect_guidance import (
|
|
12
|
+
get_zoekt_redirect_guidance,
|
|
13
|
+
get_zoekt_redirect_reason_brief,
|
|
14
|
+
)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def main() -> None:
|
|
@@ -45,7 +48,8 @@ def main() -> None:
|
|
|
45
48
|
short_label = f"blocked {block_reason}; use Zoekt MCP"
|
|
46
49
|
payload = build_block_payload(
|
|
47
50
|
brief_label=short_label,
|
|
48
|
-
permission_decision_reason=
|
|
51
|
+
permission_decision_reason=get_zoekt_redirect_reason_brief(),
|
|
52
|
+
additional_context=get_zoekt_redirect_guidance(),
|
|
49
53
|
)
|
|
50
54
|
print(json.dumps(payload))
|
|
51
55
|
sys.exit(0)
|
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
def build_block_payload(
|
|
5
5
|
brief_label: str,
|
|
6
6
|
permission_decision_reason: str,
|
|
7
|
+
additional_context: str | None = None,
|
|
7
8
|
) -> dict:
|
|
8
9
|
destructive_gate_label_prefix = "[destructive-gate]"
|
|
10
|
+
hook_specific_output: dict = {
|
|
11
|
+
"hookEventName": "PreToolUse",
|
|
12
|
+
"permissionDecision": "deny",
|
|
13
|
+
"permissionDecisionReason": permission_decision_reason,
|
|
14
|
+
}
|
|
15
|
+
if additional_context is not None:
|
|
16
|
+
hook_specific_output["additionalContext"] = additional_context
|
|
9
17
|
return {
|
|
10
|
-
"hookSpecificOutput":
|
|
11
|
-
"hookEventName": "PreToolUse",
|
|
12
|
-
"permissionDecision": "deny",
|
|
13
|
-
"permissionDecisionReason": permission_decision_reason,
|
|
14
|
-
},
|
|
18
|
+
"hookSpecificOutput": hook_specific_output,
|
|
15
19
|
"systemMessage": f"{destructive_gate_label_prefix} {brief_label}",
|
|
16
20
|
"suppressOutput": True,
|
|
17
21
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""Zoekt MCP usage and repo-to-disk path mapping for PreToolUse
|
|
1
|
+
"""Zoekt MCP usage and repo-to-disk path mapping for PreToolUse outputs."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_zoekt_redirect_reason_brief() -> str:
|
|
5
|
+
return (
|
|
6
|
+
"Use Zoekt MCP (e.g. mcp__zoekt__search) instead of Grep/Search in Zoekt-indexed trees."
|
|
7
|
+
)
|
|
2
8
|
|
|
3
9
|
|
|
4
10
|
def get_zoekt_redirect_guidance() -> str:
|
|
@@ -13,7 +13,10 @@ class ContentSearchHookIntegrationTests(unittest.TestCase):
|
|
|
13
13
|
hook_directory = pathlib.Path(__file__).resolve().parent
|
|
14
14
|
if str(hook_directory) not in sys.path:
|
|
15
15
|
sys.path.insert(0, str(hook_directory))
|
|
16
|
-
from content_search_zoekt_redirect_guidance import
|
|
16
|
+
from content_search_zoekt_redirect_guidance import (
|
|
17
|
+
get_zoekt_redirect_guidance,
|
|
18
|
+
get_zoekt_redirect_reason_brief,
|
|
19
|
+
)
|
|
17
20
|
|
|
18
21
|
hook_path = hook_directory / "content-search-to-zoekt-redirector.py"
|
|
19
22
|
destructive_gate_label_prefix = "[destructive-gate]"
|
|
@@ -38,6 +41,10 @@ class ContentSearchHookIntegrationTests(unittest.TestCase):
|
|
|
38
41
|
)
|
|
39
42
|
self.assertEqual(
|
|
40
43
|
payload["hookSpecificOutput"]["permissionDecisionReason"],
|
|
44
|
+
get_zoekt_redirect_reason_brief(),
|
|
45
|
+
)
|
|
46
|
+
self.assertEqual(
|
|
47
|
+
payload["hookSpecificOutput"]["additionalContext"],
|
|
41
48
|
get_zoekt_redirect_guidance(),
|
|
42
49
|
)
|
|
43
50
|
self.assertEqual(
|
|
@@ -11,7 +11,10 @@ if str(HOOK_DIRECTORY) not in sys.path:
|
|
|
11
11
|
sys.path.insert(0, str(HOOK_DIRECTORY))
|
|
12
12
|
|
|
13
13
|
from content_search_zoekt_block_payload import build_block_payload
|
|
14
|
-
from content_search_zoekt_redirect_guidance import
|
|
14
|
+
from content_search_zoekt_redirect_guidance import (
|
|
15
|
+
get_zoekt_redirect_guidance,
|
|
16
|
+
get_zoekt_redirect_reason_brief,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class BuildBlockPayloadTests(unittest.TestCase):
|
|
@@ -32,12 +35,14 @@ class BuildBlockPayloadTests(unittest.TestCase):
|
|
|
32
35
|
self.assertEqual(payload["suppressOutput"], True)
|
|
33
36
|
self.assertNotIn("decision", payload)
|
|
34
37
|
self.assertNotIn("reason", payload)
|
|
38
|
+
self.assertNotIn("additionalContext", payload["hookSpecificOutput"])
|
|
35
39
|
|
|
36
40
|
def test_serialized_payload_under_documented_context_cap(self) -> None:
|
|
37
41
|
cap_characters = 10_000
|
|
38
42
|
payload = build_block_payload(
|
|
39
43
|
brief_label="blocked Bash(grep); use Zoekt MCP",
|
|
40
|
-
permission_decision_reason=
|
|
44
|
+
permission_decision_reason=get_zoekt_redirect_reason_brief(),
|
|
45
|
+
additional_context=get_zoekt_redirect_guidance(),
|
|
41
46
|
)
|
|
42
47
|
serialized = json.dumps(payload)
|
|
43
48
|
self.assertLessEqual(
|
package/hooks/hooks.json
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
"description": "Code standards enforcement, safety guards, and development workflow hooks",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Grep|Search",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content-search-to-zoekt-redirector.py",
|
|
11
|
+
"timeout": 10
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
5
15
|
{
|
|
6
16
|
"matcher": "Write|Edit",
|
|
7
17
|
"hooks": [
|
|
@@ -84,6 +94,11 @@
|
|
|
84
94
|
"type": "command",
|
|
85
95
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/test-preflight-check.py",
|
|
86
96
|
"timeout": 10
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "command",
|
|
100
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content-search-to-zoekt-redirector.py",
|
|
101
|
+
"timeout": 10
|
|
87
102
|
}
|
|
88
103
|
]
|
|
89
104
|
},
|
package/package.json
CHANGED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# Prompt Workflow Hook Specs
|
|
2
|
-
|
|
3
|
-
Deterministic runtime gates for prompt workflows.
|
|
4
|
-
|
|
5
|
-
## PreToolUse Task/Agent (removed)
|
|
6
|
-
|
|
7
|
-
The former `agent-execution-intent-gate.py` hook is **removed**. Native Agent/Task launches do not carry stable custom metadata; enforcing scope text on every spawn blocked legitimate `/agent-prompt` and refinement delegations. Scope and checklist rules remain enforced by the Stop guard when a prompt-workflow response is detected.
|
|
8
|
-
|
|
9
|
-
## Gate: Leakage + Checklist + Scope (file-based validation loop)
|
|
10
|
-
|
|
11
|
-
- Validator: `hooks/blocking/prompt_workflow_validate.py`
|
|
12
|
-
- Invocation: CLI against `data/prompts/.draft-prompt.xml` (exit 0 allowed, exit 2 blocked)
|
|
13
|
-
- Fail conditions:
|
|
14
|
-
- Raw internal refinement object appears in assistant output without explicit debug intent
|
|
15
|
-
- Prompt-workflow response detected but deterministic checklist container is missing
|
|
16
|
-
- Prompt-workflow response detected and required deterministic checklist rows are missing
|
|
17
|
-
- Prompt-workflow response detected and required scope anchors are missing
|
|
18
|
-
- Prompt-workflow response detected and runtime context-control signals are missing
|
|
19
|
-
- Scope-bound text uses banned ambiguous scope terms
|
|
20
|
-
- Banned negative keywords found inside fenced XML artifact
|
|
21
|
-
- Fenced XML artifact missing required sections
|
|
22
|
-
- Enforcement: The drafting subagent writes the draft file, runs the validator, reads stderr violations (each prefixed with `[reason_code]`), edits the file, and re-runs until exit 0.
|
|
23
|
-
|
|
24
|
-
## Required Scope Anchors
|
|
25
|
-
|
|
26
|
-
- `target_local_roots`
|
|
27
|
-
- `target_canonical_roots`
|
|
28
|
-
- `target_file_globs`
|
|
29
|
-
- `comparison_basis`
|
|
30
|
-
- `completion_boundary`
|
|
31
|
-
|
|
32
|
-
## Required Deterministic Checklist Rows
|
|
33
|
-
|
|
34
|
-
- `structured_scoped_instructions`
|
|
35
|
-
- `sequential_steps_present`
|
|
36
|
-
- `positive_framing`
|
|
37
|
-
- `acceptance_criteria_defined`
|
|
38
|
-
- `safety_reversibility_language`
|
|
39
|
-
- `reversible_action_and_safety_check_guidance`
|
|
40
|
-
- `concrete_output_contract`
|
|
41
|
-
- `scope_boundary_present`
|
|
42
|
-
- `explicit_scope_anchors_present`
|
|
43
|
-
- `all_instructions_artifact_bound`
|
|
44
|
-
- `scope_terms_explicit_and_anchored`
|
|
45
|
-
- `completion_boundary_measurable`
|
|
46
|
-
- `citation_grounding_policy_present`
|
|
47
|
-
- `source_priority_rules_present`
|
|
48
|
-
|
|
49
|
-
## Runtime Context-Control Signals
|
|
50
|
-
|
|
51
|
-
- `base_minimal_instruction_layer: true`
|
|
52
|
-
- `on_demand_skill_loading: true`
|
|
53
|
-
|
|
54
|
-
These two signals are checked by the validator CLI whenever a prompt-workflow response is detected.
|
|
55
|
-
|
|
56
|
-
## Deterministic Boundary
|
|
57
|
-
|
|
58
|
-
These hooks enforce only structural/runtime checks. Semantic quality remains in auditor layer.
|
|
59
|
-
|
|
60
|
-
## Reviewing Flattened Transcript Exports
|
|
61
|
-
|
|
62
|
-
- Live prompt-workflow responses still require an explicit `Audit:` line plus one outer `xml` fence. The Stop guard and clipboard path continue to evaluate that literal boundary.
|
|
63
|
-
- Saved transcript exports can flatten blocked retry turns and omit the outer fence lines. Normalize those files with `prompt_workflow_gate_core.normalize_prompt_workflow_export(...)`, then evaluate the rebuilt message with `extract_fenced_xml_content(...)` or `extract_fenced_xml_content_from_export(...)`.
|
|
64
|
-
- Fence-relative evals review the **last successful Audit + artifact pair** after normalization. Earlier blocked retries in the flattened transcript remain diagnostic evidence and do not count as extra delivered artifacts.
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Cross-platform clipboard writes for prompt-workflow XML artifacts."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _clipboard_disabled_by_env() -> bool:
|
|
10
|
-
flag = os.environ.get("PROMPT_WORKFLOW_SKIP_CLIPBOARD", "").strip().lower()
|
|
11
|
-
return flag in {"1", "true", "yes", "on"}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def copy_text_to_system_clipboard(text: str) -> bool:
|
|
15
|
-
"""Write ``text`` to the OS clipboard using Python-only backends.
|
|
16
|
-
|
|
17
|
-
Tries :mod:`tkinter` (stdlib) first, then optional ``pyperclip`` if installed.
|
|
18
|
-
Returns ``True`` when a backend reports success, ``False`` otherwise
|
|
19
|
-
(missing dependency, headless display, empty payload, or env opt-out).
|
|
20
|
-
"""
|
|
21
|
-
if not text.strip():
|
|
22
|
-
return False
|
|
23
|
-
if _clipboard_disabled_by_env():
|
|
24
|
-
return False
|
|
25
|
-
if _copy_via_tkinter(text):
|
|
26
|
-
return True
|
|
27
|
-
return _copy_via_pyperclip(text)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _copy_via_tkinter(text: str) -> bool:
|
|
31
|
-
try:
|
|
32
|
-
import tkinter as tk
|
|
33
|
-
except ImportError:
|
|
34
|
-
return False
|
|
35
|
-
root: tk.Tk | None = None
|
|
36
|
-
try:
|
|
37
|
-
root = tk.Tk()
|
|
38
|
-
root.withdraw()
|
|
39
|
-
root.clipboard_clear()
|
|
40
|
-
root.clipboard_append(text)
|
|
41
|
-
root.update_idletasks()
|
|
42
|
-
root.update()
|
|
43
|
-
return True
|
|
44
|
-
except Exception:
|
|
45
|
-
return False
|
|
46
|
-
finally:
|
|
47
|
-
if root is not None:
|
|
48
|
-
try:
|
|
49
|
-
root.destroy()
|
|
50
|
-
except Exception:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _copy_via_pyperclip(text: str) -> bool:
|
|
55
|
-
try:
|
|
56
|
-
import pyperclip
|
|
57
|
-
except ImportError:
|
|
58
|
-
return False
|
|
59
|
-
try:
|
|
60
|
-
pyperclip.copy(text)
|
|
61
|
-
return True
|
|
62
|
-
except Exception:
|
|
63
|
-
return False
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"""Static lists and compiled regexes for prompt-workflow gate checks.
|
|
2
|
-
|
|
3
|
-
Edit this file to change scope anchors, checklist rows, markers, or keyword lists
|
|
4
|
-
without touching gate logic in prompt_workflow_gate_core.py.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
|
|
11
|
-
REQUIRED_SCOPE_ANCHORS: tuple[str, ...] = (
|
|
12
|
-
"target_local_roots",
|
|
13
|
-
"target_canonical_roots",
|
|
14
|
-
"target_file_globs",
|
|
15
|
-
"comparison_basis",
|
|
16
|
-
"completion_boundary",
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
REQUIRED_CHECKLIST_ROWS: tuple[str, ...] = (
|
|
20
|
-
"structured_scoped_instructions",
|
|
21
|
-
"sequential_steps_present",
|
|
22
|
-
"positive_framing",
|
|
23
|
-
"acceptance_criteria_defined",
|
|
24
|
-
"safety_reversibility_language",
|
|
25
|
-
"reversible_action_and_safety_check_guidance",
|
|
26
|
-
"concrete_output_contract",
|
|
27
|
-
"scope_boundary_present",
|
|
28
|
-
"explicit_scope_anchors_present",
|
|
29
|
-
"all_instructions_artifact_bound",
|
|
30
|
-
"scope_terms_explicit_and_anchored",
|
|
31
|
-
"completion_boundary_measurable",
|
|
32
|
-
"citation_grounding_policy_present",
|
|
33
|
-
"source_priority_rules_present",
|
|
34
|
-
"artifact_language_confidence",
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
AMBIGUOUS_SCOPE_TERMS: tuple[str, ...] = (
|
|
38
|
-
"this session",
|
|
39
|
-
"current files",
|
|
40
|
-
"here",
|
|
41
|
-
"above",
|
|
42
|
-
"as needed",
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
INTERNAL_OBJECT_MARKERS: tuple[str, ...] = (
|
|
46
|
-
'"pipeline_mode": "internal_section_refinement_with_final_audit"',
|
|
47
|
-
'"scope_block": {',
|
|
48
|
-
'"required_sections": [',
|
|
49
|
-
'"section_output_contract": {',
|
|
50
|
-
'"merge_output_contract": {',
|
|
51
|
-
'"audit_output_contract": {',
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
PROMPT_WORKFLOW_RESPONSE_MARKERS: tuple[str, ...] = (
|
|
55
|
-
"checklist_results",
|
|
56
|
-
"overall_status",
|
|
57
|
-
"scope anchors",
|
|
58
|
-
"target_local_roots",
|
|
59
|
-
"target_canonical_roots",
|
|
60
|
-
"target_file_globs",
|
|
61
|
-
"comparison_basis",
|
|
62
|
-
"completion_boundary",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
DEBUG_INTENT_MARKERS: tuple[str, ...] = (
|
|
66
|
-
"debug",
|
|
67
|
-
"show internal",
|
|
68
|
-
"raw internal object",
|
|
69
|
-
"pipeline object",
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
NEGATIVE_KEYWORDS_IN_ARTIFACT: tuple[str, ...] = (
|
|
73
|
-
"no",
|
|
74
|
-
"not",
|
|
75
|
-
"don't",
|
|
76
|
-
"do not",
|
|
77
|
-
"never",
|
|
78
|
-
"avoid",
|
|
79
|
-
"without",
|
|
80
|
-
"refrain",
|
|
81
|
-
"stop",
|
|
82
|
-
"prevent",
|
|
83
|
-
"exclude",
|
|
84
|
-
"prohibit",
|
|
85
|
-
"forbid",
|
|
86
|
-
"reject",
|
|
87
|
-
"cannot",
|
|
88
|
-
"unless",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
NEGATIVE_INDIRECT_PATTERNS_IN_ARTIFACT: tuple[str, ...] = (
|
|
92
|
-
r"instead of\s+\w+",
|
|
93
|
-
r"rather than\s+\w+",
|
|
94
|
-
r"as opposed to\s+\w+",
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
REQUIRED_XML_SECTIONS: tuple[str, ...] = (
|
|
98
|
-
"role",
|
|
99
|
-
"background",
|
|
100
|
-
"instructions",
|
|
101
|
-
"constraints",
|
|
102
|
-
"output_format",
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
COMPILED_NEGATIVE_KEYWORD_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
|
|
106
|
-
re.compile(rf"\b{re.escape(keyword)}\b", re.IGNORECASE)
|
|
107
|
-
for keyword in NEGATIVE_KEYWORDS_IN_ARTIFACT
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
COMPILED_NEGATIVE_INDIRECT_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
|
|
111
|
-
re.compile(pattern, re.IGNORECASE)
|
|
112
|
-
for pattern in NEGATIVE_INDIRECT_PATTERNS_IN_ARTIFACT
|
|
113
|
-
)
|
|
@@ -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]
|