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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Subprocess integration tests for content-search-to-zoekt-redirector PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContentSearchHookIntegrationTests(unittest.TestCase):
|
|
12
|
+
def test_bash_grep_command_emits_stdout_json_deny(self) -> None:
|
|
13
|
+
hook_directory = pathlib.Path(__file__).resolve().parent
|
|
14
|
+
if str(hook_directory) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(hook_directory))
|
|
16
|
+
from content_search_zoekt_redirect_guidance import get_zoekt_redirect_guidance
|
|
17
|
+
|
|
18
|
+
hook_path = hook_directory / "content-search-to-zoekt-redirector.py"
|
|
19
|
+
destructive_gate_label_prefix = "[destructive-gate]"
|
|
20
|
+
destructive_gate_label_prefix_value = f"{destructive_gate_label_prefix} "
|
|
21
|
+
expected_decision = "deny"
|
|
22
|
+
hook_stdin_payload = json.dumps(
|
|
23
|
+
{"tool_name": "Bash", "tool_input": {"command": "grep foo bar"}},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
completed = subprocess.run(
|
|
27
|
+
[sys.executable, str(hook_path)],
|
|
28
|
+
input=hook_stdin_payload,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
cwd=str(hook_directory),
|
|
32
|
+
)
|
|
33
|
+
self.assertEqual(completed.returncode, 0)
|
|
34
|
+
self.assertEqual(completed.stderr, "")
|
|
35
|
+
payload: dict[str, Any] = json.loads(completed.stdout)
|
|
36
|
+
self.assertTrue(
|
|
37
|
+
payload["systemMessage"].startswith(destructive_gate_label_prefix_value),
|
|
38
|
+
)
|
|
39
|
+
self.assertEqual(
|
|
40
|
+
payload["hookSpecificOutput"]["permissionDecisionReason"],
|
|
41
|
+
get_zoekt_redirect_guidance(),
|
|
42
|
+
)
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
payload["hookSpecificOutput"]["permissionDecision"],
|
|
45
|
+
expected_decision,
|
|
46
|
+
)
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
payload["hookSpecificOutput"]["hookEventName"],
|
|
49
|
+
"PreToolUse",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Unit tests for Zoekt redirector PreToolUse deny payload (build_block_payload)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
HOOK_DIRECTORY = pathlib.Path(__file__).resolve().parent
|
|
10
|
+
if str(HOOK_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(HOOK_DIRECTORY))
|
|
12
|
+
|
|
13
|
+
from content_search_zoekt_block_payload import build_block_payload
|
|
14
|
+
from content_search_zoekt_redirect_guidance import get_zoekt_redirect_guidance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BuildBlockPayloadTests(unittest.TestCase):
|
|
18
|
+
def test_payload_matches_pretooluse_contract(self) -> None:
|
|
19
|
+
destructive_gate_label_prefix = "[destructive-gate]"
|
|
20
|
+
payload: dict[str, Any] = build_block_payload("demo", "body")
|
|
21
|
+
prefix_with_space = f"{destructive_gate_label_prefix} "
|
|
22
|
+
self.assertTrue(payload["systemMessage"].startswith(prefix_with_space))
|
|
23
|
+
self.assertEqual(
|
|
24
|
+
payload["hookSpecificOutput"]["permissionDecisionReason"],
|
|
25
|
+
"body",
|
|
26
|
+
)
|
|
27
|
+
self.assertEqual(payload["hookSpecificOutput"]["permissionDecision"], "deny")
|
|
28
|
+
self.assertEqual(
|
|
29
|
+
payload["hookSpecificOutput"]["hookEventName"],
|
|
30
|
+
"PreToolUse",
|
|
31
|
+
)
|
|
32
|
+
self.assertEqual(payload["suppressOutput"], True)
|
|
33
|
+
self.assertNotIn("decision", payload)
|
|
34
|
+
self.assertNotIn("reason", payload)
|
|
35
|
+
|
|
36
|
+
def test_serialized_payload_under_documented_context_cap(self) -> None:
|
|
37
|
+
cap_characters = 10_000
|
|
38
|
+
payload = build_block_payload(
|
|
39
|
+
brief_label="blocked Bash(grep); use Zoekt MCP",
|
|
40
|
+
permission_decision_reason=get_zoekt_redirect_guidance(),
|
|
41
|
+
)
|
|
42
|
+
serialized = json.dumps(payload)
|
|
43
|
+
self.assertLessEqual(
|
|
44
|
+
len(serialized),
|
|
45
|
+
cap_characters,
|
|
46
|
+
msg="Hooks doc caps additionalContext/systemMessage/plain stdout injection at 10,000 characters",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
unittest.main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for Zoekt indexed root resolution (env, file, empty built-in fallback, WSL variants)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
HOOK_DIRECTORY = pathlib.Path(__file__).resolve().parent
|
|
12
|
+
if str(HOOK_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(HOOK_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
from content_search_zoekt_indexed_paths import is_in_indexed_repo
|
|
16
|
+
from content_search_zoekt_indexed_roots_config import (
|
|
17
|
+
clear_indexed_root_prefixes_cache,
|
|
18
|
+
indexed_root_prefixes,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IndexedRootsConfigTests(unittest.TestCase):
|
|
23
|
+
def tearDown(self) -> None:
|
|
24
|
+
clear_indexed_root_prefixes_cache()
|
|
25
|
+
|
|
26
|
+
def test_environment_json_array_defines_prefixes(self) -> None:
|
|
27
|
+
roots_json = json.dumps(["Y:/OnlyOne/Indexed/"])
|
|
28
|
+
with patch.dict(os.environ, {"ZOEKT_REDIRECT_INDEXED_ROOTS": roots_json}, clear=False):
|
|
29
|
+
clear_indexed_root_prefixes_cache()
|
|
30
|
+
prefixes = indexed_root_prefixes()
|
|
31
|
+
self.assertIn("y:/onlyone/indexed/", prefixes)
|
|
32
|
+
self.assertIn("/mnt/y/onlyone/indexed/", prefixes)
|
|
33
|
+
|
|
34
|
+
def test_empty_environment_array_yields_no_prefixes(self) -> None:
|
|
35
|
+
with patch.dict(os.environ, {"ZOEKT_REDIRECT_INDEXED_ROOTS": "[]"}, clear=False):
|
|
36
|
+
clear_indexed_root_prefixes_cache()
|
|
37
|
+
prefixes = indexed_root_prefixes()
|
|
38
|
+
self.assertEqual(prefixes, ())
|
|
39
|
+
|
|
40
|
+
def test_json_file_used_when_env_missing(self) -> None:
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmp_str:
|
|
42
|
+
home = pathlib.Path(tmp_str)
|
|
43
|
+
config_dir = home / ".claude"
|
|
44
|
+
config_dir.mkdir(parents=True)
|
|
45
|
+
roots_payload = {"roots": ["Y:/FromFile/Project/"]}
|
|
46
|
+
(config_dir / "zoekt-indexed-roots.json").write_text(
|
|
47
|
+
json.dumps(roots_payload),
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
with patch("pathlib.Path.home", return_value=home):
|
|
51
|
+
saved = os.environ.pop("ZOEKT_REDIRECT_INDEXED_ROOTS", None)
|
|
52
|
+
try:
|
|
53
|
+
clear_indexed_root_prefixes_cache()
|
|
54
|
+
prefixes = indexed_root_prefixes()
|
|
55
|
+
finally:
|
|
56
|
+
if saved is not None:
|
|
57
|
+
os.environ["ZOEKT_REDIRECT_INDEXED_ROOTS"] = saved
|
|
58
|
+
self.assertIn("y:/fromfile/project/", prefixes)
|
|
59
|
+
|
|
60
|
+
def test_environment_overrides_file(self) -> None:
|
|
61
|
+
with tempfile.TemporaryDirectory() as tmp_str:
|
|
62
|
+
home = pathlib.Path(tmp_str)
|
|
63
|
+
config_dir = home / ".claude"
|
|
64
|
+
config_dir.mkdir(parents=True)
|
|
65
|
+
(config_dir / "zoekt-indexed-roots.json").write_text(
|
|
66
|
+
json.dumps({"roots": ["Y:/FromFile/"]}),
|
|
67
|
+
encoding="utf-8",
|
|
68
|
+
)
|
|
69
|
+
with patch("pathlib.Path.home", return_value=home):
|
|
70
|
+
with patch.dict(
|
|
71
|
+
os.environ,
|
|
72
|
+
{"ZOEKT_REDIRECT_INDEXED_ROOTS": json.dumps(["Y:/FromEnv/"])},
|
|
73
|
+
clear=False,
|
|
74
|
+
):
|
|
75
|
+
clear_indexed_root_prefixes_cache()
|
|
76
|
+
prefixes = indexed_root_prefixes()
|
|
77
|
+
self.assertIn("y:/fromenv/", prefixes)
|
|
78
|
+
self.assertNotIn("y:/fromfile/", prefixes)
|
|
79
|
+
|
|
80
|
+
def test_longer_prefix_matches_before_shorter_parent(self) -> None:
|
|
81
|
+
roots_json = json.dumps(["Y:/parent/", "Y:/parent/child/"])
|
|
82
|
+
with patch.dict(os.environ, {"ZOEKT_REDIRECT_INDEXED_ROOTS": roots_json}, clear=False):
|
|
83
|
+
clear_indexed_root_prefixes_cache()
|
|
84
|
+
self.assertTrue(is_in_indexed_repo("Y:/parent/child/file.py"))
|
|
85
|
+
|
|
86
|
+
def test_invalid_environment_json_falls_through_to_empty_builtin(self) -> None:
|
|
87
|
+
with patch(
|
|
88
|
+
"content_search_zoekt_indexed_roots_config._roots_from_json_file",
|
|
89
|
+
return_value=None,
|
|
90
|
+
):
|
|
91
|
+
with patch.dict(
|
|
92
|
+
os.environ,
|
|
93
|
+
{"ZOEKT_REDIRECT_INDEXED_ROOTS": "not-json"},
|
|
94
|
+
clear=False,
|
|
95
|
+
):
|
|
96
|
+
clear_indexed_root_prefixes_cache()
|
|
97
|
+
prefixes = indexed_root_prefixes()
|
|
98
|
+
self.assertEqual(prefixes, ())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
unittest.main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for destructive-command-blocker hook."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SCRIPT_PATH = Path(__file__).parent / "destructive-command-blocker.py"
|
|
10
|
+
GH_GATE_USER_FACING_PREFIX = "[gh-gate]"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
14
|
+
return subprocess.run(
|
|
15
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
16
|
+
input=json.dumps(payload),
|
|
17
|
+
text=True,
|
|
18
|
+
capture_output=True,
|
|
19
|
+
check=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_bash_payload(command: str) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"tool_name": "Bash",
|
|
26
|
+
"tool_input": {"command": command},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_denies_gh_issue_comment_as_redirect_duplicate_guard() -> None:
|
|
31
|
+
payload = _make_bash_payload(
|
|
32
|
+
'gh issue comment 83 --repo jl-cmd/claude-code-config --body "hello"'
|
|
33
|
+
)
|
|
34
|
+
result = _run_hook(payload)
|
|
35
|
+
response = json.loads(result.stdout)
|
|
36
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
37
|
+
assert (
|
|
38
|
+
"gh issue comment" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
39
|
+
)
|
|
40
|
+
assert (
|
|
41
|
+
"duplicate execution"
|
|
42
|
+
in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_denies_gh_pr_comment_as_redirect_duplicate_guard() -> None:
|
|
47
|
+
payload = _make_bash_payload('gh pr comment 42 --body "ok"')
|
|
48
|
+
result = _run_hook(payload)
|
|
49
|
+
response = json.loads(result.stdout)
|
|
50
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
51
|
+
assert "gh pr comment" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_denies_gh_pr_review_as_redirect_duplicate_guard() -> None:
|
|
55
|
+
payload = _make_bash_payload("gh pr review 42 --approve")
|
|
56
|
+
result = _run_hook(payload)
|
|
57
|
+
response = json.loads(result.stdout)
|
|
58
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
59
|
+
assert "gh pr review" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_denies_gh_api_post_comment_as_redirect_duplicate_guard() -> None:
|
|
63
|
+
payload = _make_bash_payload(
|
|
64
|
+
"gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
|
|
65
|
+
)
|
|
66
|
+
result = _run_hook(payload)
|
|
67
|
+
response = json.loads(result.stdout)
|
|
68
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_suppresses_output_on_gh_redirect_deny() -> None:
|
|
72
|
+
payload = _make_bash_payload('gh issue comment 1 --body "x"')
|
|
73
|
+
result = _run_hook(payload)
|
|
74
|
+
response = json.loads(result.stdout)
|
|
75
|
+
assert response["suppressOutput"] is True
|
|
76
|
+
assert response["systemMessage"].startswith(GH_GATE_USER_FACING_PREFIX)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_asks_on_rm_rf_still_works() -> None:
|
|
80
|
+
payload = _make_bash_payload("rm -rf /tmp/somewhere")
|
|
81
|
+
result = _run_hook(payload)
|
|
82
|
+
response = json.loads(result.stdout)
|
|
83
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
84
|
+
assert "rm -rf" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_asks_on_git_push_force_still_works() -> None:
|
|
88
|
+
payload = _make_bash_payload("git push --force origin main")
|
|
89
|
+
result = _run_hook(payload)
|
|
90
|
+
response = json.loads(result.stdout)
|
|
91
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
92
|
+
assert (
|
|
93
|
+
"git push --force" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_allows_plain_command_without_destructive_pattern() -> None:
|
|
98
|
+
payload = _make_bash_payload("ls -la")
|
|
99
|
+
result = _run_hook(payload)
|
|
100
|
+
assert result.stdout.strip() == ""
|
|
101
|
+
assert result.returncode == 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_ignores_non_bash_tool() -> None:
|
|
105
|
+
payload = {"tool_name": "Read", "tool_input": {"file_path": "/tmp/x"}}
|
|
106
|
+
result = _run_hook(payload)
|
|
107
|
+
assert result.stdout.strip() == ""
|
|
108
|
+
assert result.returncode == 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"tdd",
|
|
23
23
|
"code-quality"
|
|
24
24
|
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@jl-cmd/prompt-generator": "^1.0.0"
|
|
27
|
+
},
|
|
25
28
|
"license": "MIT",
|
|
26
29
|
"repository": {
|
|
27
30
|
"type": "git",
|
|
@@ -89,8 +89,8 @@ For EACH hook entry in settings.json (both layers):
|
|
|
89
89
|
- hook_script_path (extract from the command string after the last quote)
|
|
90
90
|
- Read the actual script file
|
|
91
91
|
- purpose (what rule does this hook enforce?)
|
|
92
|
-
- enforcement_type: "blocking" (exit 2
|
|
93
|
-
- method: "
|
|
92
|
+
- enforcement_type: "blocking" (exit 2 stderr, or PreToolUse exit 0 + JSON deny) | "advisory" (stdout message) | "validation" (post-check)
|
|
93
|
+
- method: "exit_code_2_stderr" | "pretooluse_json_stdout" (hookSpecificOutput.permissionDecision; see https://code.claude.com/docs/en/hooks) | "stdout" | "other"
|
|
94
94
|
- which_rule_file (which .Codex/rules/*.md or AGENTS.md rule does this correspond to?)
|
|
95
95
|
- orphaned (hook exists on disk but NOT in settings.json?)
|
|
96
96
|
```
|
|
@@ -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
|
-
)
|