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.
Files changed (25) hide show
  1. package/hooks/blocking/content-search-to-zoekt-redirector.py +6 -2
  2. package/hooks/blocking/content_search_zoekt_block_payload.py +9 -5
  3. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +7 -1
  4. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +8 -1
  5. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +7 -2
  6. package/hooks/hooks.json +15 -0
  7. package/package.json +1 -1
  8. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +0 -64
  9. package/hooks/blocking/prompt_workflow_clipboard.py +0 -63
  10. package/hooks/blocking/prompt_workflow_gate_config.py +0 -113
  11. package/hooks/blocking/prompt_workflow_gate_core.py +0 -289
  12. package/hooks/blocking/prompt_workflow_validate.py +0 -218
  13. package/hooks/blocking/test_prompt_workflow_clipboard.py +0 -54
  14. package/hooks/blocking/test_prompt_workflow_gate_core.py +0 -195
  15. package/hooks/blocking/test_prompt_workflow_validate.py +0 -339
  16. package/rules/prompt-workflow-context-controls.md +0 -48
  17. package/skills/agent-prompt/SKILL.md +0 -199
  18. package/skills/prompt-generator/ARCHITECTURE.md +0 -18
  19. package/skills/prompt-generator/REFERENCE.md +0 -254
  20. package/skills/prompt-generator/REFINEMENT_PIPELINE_RUNBOOK.md +0 -177
  21. package/skills/prompt-generator/SKILL.md +0 -354
  22. package/skills/prompt-generator/TARGET_OUTPUT.md +0 -133
  23. package/skills/prompt-generator/evals/prompt-generator.json +0 -207
  24. package/skills/prompt-generator/templates/skill-from-ground-up.md +0 -104
  25. 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 get_zoekt_redirect_guidance
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=get_zoekt_redirect_guidance(),
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 permissionDecisionReason."""
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 get_zoekt_redirect_guidance
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 get_zoekt_redirect_guidance
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=get_zoekt_redirect_guidance(),
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,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.17.5",
3
+ "version": "1.19.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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]