claude-dev-env 8.1.0 → 8.3.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/.agents/agents/poteto-agent.md +6 -0
- package/.agents/agents/test_agent_frontmatter.py +9 -9
- package/.agents/agents/test_pr_description_writer_style.py +1 -1
- package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
- package/.agents/{agents → agents-archived}/clean-coder.md +4 -4
- package/.agents/{agents → agents-archived}/code-quality-agent.md +5 -5
- package/.agents/{agents → agents-archived}/plan-packet-validator.md +2 -2
- package/.agents/{agents → agents-archived}/session-advisor.md +2 -2
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
- package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
- package/.agents/skills/e-code-review/SKILL.md +2 -2
- package/.agents/skills/e-code-review/reference/fix.md +1 -1
- package/.agents/skills/e-code-review/reference/loop.md +1 -1
- package/.agents/skills/e-code-review/reference/medium.md +4 -4
- package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/grok-spawn/SKILL.md +9 -6
- package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
- package/.agents/skills/issue-tracker/SKILL.md +3 -3
- package/.agents/skills/orchestrator/SKILL.md +19 -18
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
- package/.agents/skills/orchestrator-refresh/SKILL.md +4 -2
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
- package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
- package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
- package/.agents/skills/pull-request/SKILL.md +14 -5
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
- package/.agents/skills/syncing-submodules/SKILL.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +11 -9
- package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
- package/.agents/skills/usage-pause/SKILL.md +3 -3
- package/_shared/advisor/advisor-protocol.md +7 -3
- package/_shared/advisor/reference/advisor-block.md +8 -1
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
- package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +5 -5
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +4 -0
- package/bin/install.agents-home.test.mjs +44 -94
- package/bin/install.mjs +31 -24
- package/bin/install.test.mjs +21 -0
- package/bin/resolve-package-managed-directory.test.mjs +1 -1
- package/bin/subagent-model-policy.test.mjs +15 -8
- package/docs/BDD_SCENARIO_QUALITY.md +2 -2
- package/docs/CODE_RULES.md +7 -7
- package/docs/TEST_QUALITY.md +1 -1
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/advisor-tool.md +2 -2
- package/docs/references/prose-style-enforcement.md +1 -1
- package/docs/references/team-advisor-skill.md +2 -2
- package/docs/references/weak-executor-advisor.md +1 -1
- package/docs/subagent-model-routing.md +6 -5
- package/hooks/blocking/tdd_enforcer.py +2 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
- package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
- package/hooks/routing/subagent_model_routing.mjs +15 -0
- package/hooks/routing/subagent_model_routing.test.mjs +58 -25
- package/package.json +1 -1
- package/rules/archiving-agent-config.md +5 -8
- package/rules/git-workflow.md +3 -3
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/shell-invocation.md +0 -2
- package/rules/subagent-model-policy.json +5 -13
- package/rules/testing.md +0 -2
- package/scripts/active_capability_references.py +74 -19
- package/scripts/banned_prose_words.py +159 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
- package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
- package/scripts/grok_headless_runner.py +19 -1
- package/scripts/grok_worker_preflight.py +67 -1
- package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
- package/scripts/policy_lint/adapters.py +6 -6
- package/scripts/policy_lint/config/constants.py +8 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/resolve_advisor_model_route.mjs +2 -2
- package/scripts/subagent_model_policy.mjs +4 -1
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_active_capability_references.py +100 -33
- package/scripts/test_grok_headless_runner.py +35 -1
- package/scripts/test_grok_worker_preflight.py +73 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
- package/scripts/tests/test_banned_prose_words.py +103 -0
- package/scripts/tests/test_engine.py +1 -1
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
- package/scripts/tests/test_rules.py +17 -6
- package/system-prompts/software-engineer.xml +7 -7
- package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
- package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
- package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
- package/hooks/observability/test_failure_recorder.py +0 -221
- package/hooks/observability/test_test_failure_recorder.py +0 -173
- /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
- /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
- /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
- /package/.agents/{agents → agents-archived}/skill-writer-agent.md +0 -0
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
Builds inventories from committed skill manifests, agent markdown files, and
|
|
5
5
|
commands. Scans active prompt text for slash and backticked capability names.
|
|
6
|
-
Fails when an active (non-inert) reference names a
|
|
6
|
+
Fails when an active (non-inert) reference names a capability the tree
|
|
7
|
+
dropped, resolved from the ever-shipped registry against what the tree
|
|
8
|
+
holds today.
|
|
7
9
|
|
|
8
10
|
::
|
|
9
11
|
|
|
10
12
|
unresolved = unresolved_active_capabilities(package_root)
|
|
11
13
|
ok: unresolved == []
|
|
12
|
-
flag: active text mentions
|
|
14
|
+
flag: active text mentions a name the tree dropped
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
from __future__ import annotations
|
|
@@ -19,10 +21,12 @@ from dataclasses import dataclass
|
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
|
|
21
23
|
from dev_env_scripts_constants.active_capability_constants import (
|
|
22
|
-
ALL_BANNED_ACTIVE_CAPABILITY_NAMES,
|
|
23
24
|
ALL_INERT_FENCE_LANGUAGES,
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
ALL_QUALIFIED_CAPABILITY_PATTERNS,
|
|
26
|
+
ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES,
|
|
27
|
+
BLANKED_SPAN,
|
|
28
|
+
EVER_SHIPPED_NAME_PATTERN,
|
|
29
|
+
EVER_SHIPPED_REGISTRY_RELATIVE_PATH,
|
|
26
30
|
FENCE_CLOSE_PATTERN,
|
|
27
31
|
FENCE_OPEN_PATTERN,
|
|
28
32
|
NEWLINE_JOIN_SEPARATOR,
|
|
@@ -31,8 +35,11 @@ from dev_env_scripts_constants.active_capability_constants import (
|
|
|
31
35
|
PACKAGE_ROOT_AGENTS_DIRECTORY,
|
|
32
36
|
PACKAGE_ROOT_SKILLS_DIRECTORY,
|
|
33
37
|
PACKAGE_SKILLS_DIRECTORY,
|
|
38
|
+
RETIRED_REASON_PREFIX,
|
|
34
39
|
SKILL_MANIFEST_FILENAME,
|
|
40
|
+
MULTI_SEGMENT_PATH_PATTERN,
|
|
35
41
|
SLASH_CAPABILITY_PATTERN,
|
|
42
|
+
URL_PATTERN,
|
|
36
43
|
UTF8_ENCODING,
|
|
37
44
|
)
|
|
38
45
|
|
|
@@ -52,7 +59,7 @@ class CapabilityInventory:
|
|
|
52
59
|
|
|
53
60
|
@dataclass(frozen=True)
|
|
54
61
|
class UnresolvedCapabilityReference:
|
|
55
|
-
"""One active reference that names a
|
|
62
|
+
"""One active reference that names a capability outside the shipped tree."""
|
|
56
63
|
|
|
57
64
|
file_path: str
|
|
58
65
|
line_number: int
|
|
@@ -148,6 +155,10 @@ def strip_inert_fenced_blocks(markdown_text: str) -> str:
|
|
|
148
155
|
def extract_active_capability_names(markdown_text: str) -> list[tuple[int, str]]:
|
|
149
156
|
"""Return (1-based line, capability name) for active references.
|
|
150
157
|
|
|
158
|
+
A slash reference counts as an invocation, and a qualified reference
|
|
159
|
+
names a skill directory or manifest. A URL and a multi-segment path
|
|
160
|
+
carry neither, so both are blanked before the scan.
|
|
161
|
+
|
|
151
162
|
Args:
|
|
152
163
|
markdown_text: Prompt or skill markdown.
|
|
153
164
|
|
|
@@ -155,31 +166,72 @@ def extract_active_capability_names(markdown_text: str) -> list[tuple[int, str]]
|
|
|
155
166
|
Ordered list of line number and capability name pairs.
|
|
156
167
|
"""
|
|
157
168
|
slash_pattern = re.compile(SLASH_CAPABILITY_PATTERN)
|
|
158
|
-
|
|
169
|
+
url_pattern = re.compile(URL_PATTERN)
|
|
170
|
+
path_pattern = re.compile(MULTI_SEGMENT_PATH_PATTERN)
|
|
171
|
+
all_qualified_patterns = [
|
|
172
|
+
re.compile(each_pattern) for each_pattern in ALL_QUALIFIED_CAPABILITY_PATTERNS
|
|
173
|
+
]
|
|
159
174
|
active_text = strip_inert_fenced_blocks(markdown_text)
|
|
160
175
|
all_hits: list[tuple[int, str]] = []
|
|
161
176
|
for each_line_number, each_line in enumerate(active_text.splitlines(), start=1):
|
|
162
|
-
for
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
for each_pattern in all_qualified_patterns:
|
|
178
|
+
for each_match in each_pattern.finditer(each_line):
|
|
179
|
+
all_hits.append((each_line_number, each_match.group(1)))
|
|
180
|
+
invocation_line = path_pattern.sub(
|
|
181
|
+
BLANKED_SPAN, url_pattern.sub(BLANKED_SPAN, each_line)
|
|
182
|
+
)
|
|
183
|
+
for each_match in slash_pattern.finditer(invocation_line):
|
|
165
184
|
all_hits.append((each_line_number, each_match.group(1)))
|
|
166
185
|
return all_hits
|
|
167
186
|
|
|
168
187
|
|
|
169
|
-
def
|
|
170
|
-
|
|
188
|
+
def retired_capability_names(
|
|
189
|
+
from_package_root: Path, inventory: CapabilityInventory
|
|
190
|
+
) -> frozenset[str]:
|
|
191
|
+
"""Return the capability names the shipped tree dropped.
|
|
171
192
|
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
The ever-shipped registry keeps every skill name the package released.
|
|
194
|
+
Subtracting what the tree holds today leaves the retired set, so no hand
|
|
195
|
+
kept list decides which name fails.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
from_package_root: ``packages/claude-dev-env`` root.
|
|
199
|
+
inventory: Names the tree holds today.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Retired capability names.
|
|
203
|
+
"""
|
|
204
|
+
registry_path = from_package_root / EVER_SHIPPED_REGISTRY_RELATIVE_PATH
|
|
205
|
+
all_ever_shipped_names: set[str] = set(
|
|
206
|
+
ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES
|
|
207
|
+
)
|
|
208
|
+
if registry_path.is_file():
|
|
209
|
+
all_ever_shipped_names.update(
|
|
210
|
+
re.findall(
|
|
211
|
+
EVER_SHIPPED_NAME_PATTERN,
|
|
212
|
+
registry_path.read_text(encoding=UTF8_ENCODING),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return frozenset(all_ever_shipped_names - inventory.all_known_names())
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def classify_capability_reference(
|
|
219
|
+
capability_name: str, all_retired_names: frozenset[str]
|
|
220
|
+
) -> str | None:
|
|
221
|
+
"""Return a failure reason when the capability left the tree.
|
|
222
|
+
|
|
223
|
+
Extractors also match ordinary prose tokens, so a name the package never
|
|
224
|
+
shipped passes. A name the registry records and the tree dropped fails.
|
|
174
225
|
|
|
175
226
|
Args:
|
|
176
227
|
capability_name: Extracted skill/command-like name.
|
|
228
|
+
all_retired_names: Names resolved by ``retired_capability_names``.
|
|
177
229
|
|
|
178
230
|
Returns:
|
|
179
231
|
Reason string, or None when the reference is allowed.
|
|
180
232
|
"""
|
|
181
|
-
if capability_name in
|
|
182
|
-
return f"{
|
|
233
|
+
if capability_name in all_retired_names:
|
|
234
|
+
return f"{RETIRED_REASON_PREFIX}{capability_name}"
|
|
183
235
|
return None
|
|
184
236
|
|
|
185
237
|
|
|
@@ -188,7 +240,7 @@ def unresolved_active_capabilities(
|
|
|
188
240
|
*,
|
|
189
241
|
all_relative_markdown_paths: list[str] | None = None,
|
|
190
242
|
) -> list[UnresolvedCapabilityReference]:
|
|
191
|
-
"""Scan package markdown for
|
|
243
|
+
"""Scan package markdown for references to retired capabilities.
|
|
192
244
|
|
|
193
245
|
Args:
|
|
194
246
|
from_package_root: ``packages/claude-dev-env`` root.
|
|
@@ -196,10 +248,13 @@ def unresolved_active_capabilities(
|
|
|
196
248
|
defaults to all skills, agents, and commands markdown.
|
|
197
249
|
|
|
198
250
|
Returns:
|
|
199
|
-
|
|
251
|
+
Retired references with file, line, name, and reason.
|
|
200
252
|
"""
|
|
201
253
|
if all_relative_markdown_paths is None:
|
|
202
254
|
all_relative_markdown_paths = _default_markdown_paths(from_package_root)
|
|
255
|
+
all_retired_names = retired_capability_names(
|
|
256
|
+
from_package_root, build_capability_inventory(from_package_root)
|
|
257
|
+
)
|
|
203
258
|
all_unresolved: list[UnresolvedCapabilityReference] = []
|
|
204
259
|
for each_relative_path in all_relative_markdown_paths:
|
|
205
260
|
absolute_path = from_package_root / each_relative_path
|
|
@@ -209,7 +264,7 @@ def unresolved_active_capabilities(
|
|
|
209
264
|
for each_line_number, each_name in extract_active_capability_names(
|
|
210
265
|
markdown_text
|
|
211
266
|
):
|
|
212
|
-
reason = classify_capability_reference(each_name)
|
|
267
|
+
reason = classify_capability_reference(each_name, all_retired_names)
|
|
213
268
|
if reason is None:
|
|
214
269
|
continue
|
|
215
270
|
all_unresolved.append(
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Find banned emphasis words in authored prose.
|
|
3
|
+
|
|
4
|
+
The package AGENTS.md bans ``real``, ``really``, ``real-world``, ``actual``,
|
|
5
|
+
``actually``, ``genuine``, and ``true`` from every authored surface. This
|
|
6
|
+
module reports each occurrence that survives after code spans, paths, and
|
|
7
|
+
link targets are removed, so a caller can grade a document or a whole tree.
|
|
8
|
+
|
|
9
|
+
::
|
|
10
|
+
|
|
11
|
+
all_hits = find_banned_prose_words("The real cause is a timeout.\n")
|
|
12
|
+
ok: all_hits == [(1, "real")]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
|
+
|
|
19
|
+
from dev_env_scripts_constants.banned_prose_word_constants import (
|
|
20
|
+
ADJECTIVAL_TRUE_PATTERN,
|
|
21
|
+
ALL_EXEMPT_PATH_SEGMENTS,
|
|
22
|
+
ALL_GOVERNED_PATH_SEGMENTS,
|
|
23
|
+
ALL_GOVERNED_SUFFIXES,
|
|
24
|
+
ALL_PREDICATIVE_TRUE_LEADS,
|
|
25
|
+
BANNED_PROSE_WORD_PATTERN,
|
|
26
|
+
BLANKED_SPAN,
|
|
27
|
+
FENCED_CODE_PATTERN,
|
|
28
|
+
INDENTED_CODE_PATTERN,
|
|
29
|
+
INLINE_CODE_PATTERN,
|
|
30
|
+
MARKDOWN_LINK_TARGET_PATTERN,
|
|
31
|
+
PATH_PATTERN,
|
|
32
|
+
URL_PATTERN,
|
|
33
|
+
UTF8_ENCODING,
|
|
34
|
+
XML_TAG_PATTERN,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def governs_path(relative_posix_path: str) -> bool:
|
|
39
|
+
"""Return whether the ban applies to one repository path.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
relative_posix_path: Repository-relative path with forward slashes.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True for an authored instruction surface the ban covers.
|
|
46
|
+
"""
|
|
47
|
+
normalized_path = f"/{relative_posix_path.lower().lstrip('/')}"
|
|
48
|
+
if PurePosixPath(normalized_path).suffix not in ALL_GOVERNED_SUFFIXES:
|
|
49
|
+
return False
|
|
50
|
+
if any(
|
|
51
|
+
each_segment in normalized_path for each_segment in ALL_EXEMPT_PATH_SEGMENTS
|
|
52
|
+
):
|
|
53
|
+
return False
|
|
54
|
+
return any(
|
|
55
|
+
each_segment in normalized_path for each_segment in ALL_GOVERNED_PATH_SEGMENTS
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def prose_lines(document_text: str) -> list[str]:
|
|
60
|
+
"""Return one prose line for each source line, with code spans blanked.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
document_text: Full document source.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Lines of the same count as the source, holding prose only.
|
|
67
|
+
"""
|
|
68
|
+
all_prose_lines: list[str] = []
|
|
69
|
+
is_inside_fence = False
|
|
70
|
+
for each_line in document_text.splitlines():
|
|
71
|
+
if FENCED_CODE_PATTERN.match(each_line):
|
|
72
|
+
is_inside_fence = not is_inside_fence
|
|
73
|
+
all_prose_lines.append("")
|
|
74
|
+
continue
|
|
75
|
+
if is_inside_fence or INDENTED_CODE_PATTERN.match(each_line):
|
|
76
|
+
all_prose_lines.append("")
|
|
77
|
+
continue
|
|
78
|
+
prose_line = INLINE_CODE_PATTERN.sub(BLANKED_SPAN, each_line)
|
|
79
|
+
prose_line = XML_TAG_PATTERN.sub(BLANKED_SPAN, prose_line)
|
|
80
|
+
prose_line = MARKDOWN_LINK_TARGET_PATTERN.sub(BLANKED_SPAN, prose_line)
|
|
81
|
+
prose_line = URL_PATTERN.sub(BLANKED_SPAN, prose_line)
|
|
82
|
+
all_prose_lines.append(PATH_PATTERN.sub(BLANKED_SPAN, prose_line))
|
|
83
|
+
return all_prose_lines
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _adjectival_true_columns(prose_line: str) -> list[int]:
|
|
87
|
+
all_columns: list[int] = []
|
|
88
|
+
for each_match in ADJECTIVAL_TRUE_PATTERN.finditer(prose_line):
|
|
89
|
+
leading_words = prose_line[: each_match.start()].lower().split()
|
|
90
|
+
if leading_words and leading_words[-1].strip(",;:(") in (
|
|
91
|
+
ALL_PREDICATIVE_TRUE_LEADS
|
|
92
|
+
):
|
|
93
|
+
continue
|
|
94
|
+
all_columns.append(each_match.start() + 1)
|
|
95
|
+
return all_columns
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_banned_prose_words(document_text: str) -> list[tuple[int, int, str]]:
|
|
99
|
+
"""Return each banned word occurrence in authored prose.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
document_text: Full document source.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Ordered ``(line number, column, word)`` triples, both 1-based.
|
|
106
|
+
"""
|
|
107
|
+
all_hits: list[tuple[int, int, str]] = []
|
|
108
|
+
for each_line_number, each_prose_line in enumerate(prose_lines(document_text), 1):
|
|
109
|
+
for each_match in BANNED_PROSE_WORD_PATTERN.finditer(each_prose_line):
|
|
110
|
+
all_hits.append(
|
|
111
|
+
(each_line_number, each_match.start() + 1, each_match.group(1).lower())
|
|
112
|
+
)
|
|
113
|
+
for each_column in _adjectival_true_columns(each_prose_line):
|
|
114
|
+
all_hits.append((each_line_number, each_column, "true"))
|
|
115
|
+
return sorted(all_hits)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def governed_paths(from_package_root: Path) -> list[str]:
|
|
119
|
+
"""Return every governed surface under one package root.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
from_package_root: ``packages/claude-dev-env`` root.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Sorted package-relative paths the ban covers.
|
|
126
|
+
"""
|
|
127
|
+
all_paths: list[str] = []
|
|
128
|
+
for each_path in from_package_root.rglob("*"):
|
|
129
|
+
if not each_path.is_file():
|
|
130
|
+
continue
|
|
131
|
+
relative_path = each_path.relative_to(from_package_root).as_posix()
|
|
132
|
+
if governs_path(relative_path):
|
|
133
|
+
all_paths.append(relative_path)
|
|
134
|
+
return sorted(all_paths)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def banned_prose_words_in_tree(
|
|
138
|
+
from_package_root: Path,
|
|
139
|
+
) -> list[tuple[str, int, int, str]]:
|
|
140
|
+
"""Return every banned word occurrence across the governed surfaces.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
from_package_root: ``packages/claude-dev-env`` root.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Ordered ``(package-relative path, line number, column, word)`` tuples.
|
|
147
|
+
"""
|
|
148
|
+
all_hits: list[tuple[str, int, int, str]] = []
|
|
149
|
+
for each_relative_path in governed_paths(from_package_root):
|
|
150
|
+
document_text = (from_package_root / each_relative_path).read_text(
|
|
151
|
+
encoding=UTF8_ENCODING, errors="replace"
|
|
152
|
+
)
|
|
153
|
+
for each_line_number, each_column, each_word in find_banned_prose_words(
|
|
154
|
+
document_text
|
|
155
|
+
):
|
|
156
|
+
all_hits.append(
|
|
157
|
+
(each_relative_path, each_line_number, each_column, each_word)
|
|
158
|
+
)
|
|
159
|
+
return all_hits
|
|
@@ -14,18 +14,16 @@ PACKAGE_ROOT_SKILLS_DIRECTORY: str = "skills"
|
|
|
14
14
|
PACKAGE_ROOT_AGENTS_DIRECTORY: str = "agents"
|
|
15
15
|
SKILL_MANIFEST_FILENAME: str = "SKILL.md"
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
ALL_BANNED_ACTIVE_CAPABILITY_NAMES: frozenset[str] = frozenset(
|
|
17
|
+
ALL_UNREGISTERED_RETIRED_CAPABILITY_NAMES: frozenset[str] = frozenset(
|
|
19
18
|
{
|
|
20
19
|
"stub-detector",
|
|
21
20
|
"superpowers",
|
|
22
|
-
"qbug",
|
|
23
|
-
"findbugs",
|
|
24
|
-
"fixbugs",
|
|
25
|
-
"monitor-open-prs",
|
|
26
21
|
}
|
|
27
22
|
)
|
|
28
23
|
|
|
24
|
+
EVER_SHIPPED_REGISTRY_RELATIVE_PATH: str = "bin/ever-shipped-skills.mjs"
|
|
25
|
+
EVER_SHIPPED_NAME_PATTERN: str = r"'([a-z][a-z0-9-]*)'"
|
|
26
|
+
|
|
29
27
|
# Fence languages treated as inert historical/example content.
|
|
30
28
|
ALL_INERT_FENCE_LANGUAGES: frozenset[str] = frozenset(
|
|
31
29
|
{
|
|
@@ -37,13 +35,17 @@ ALL_INERT_FENCE_LANGUAGES: frozenset[str] = frozenset(
|
|
|
37
35
|
}
|
|
38
36
|
)
|
|
39
37
|
|
|
40
|
-
SLASH_CAPABILITY_PATTERN: str = r"(?<![`\w])/([a-z][a-z0-9-]{2,})"
|
|
41
|
-
|
|
42
|
-
r"
|
|
38
|
+
SLASH_CAPABILITY_PATTERN: str = r"(?<![`\w./-])/([a-z][a-z0-9-]{2,})(?![\w/-])"
|
|
39
|
+
ALL_QUALIFIED_CAPABILITY_PATTERNS: tuple[str, ...] = (
|
|
40
|
+
r"(?<![\w-])([a-z][a-z0-9-]{2,})/SKILL\.md",
|
|
41
|
+
r"`skills/([a-z][a-z0-9-]{2,})`",
|
|
43
42
|
)
|
|
43
|
+
URL_PATTERN: str = r"\b[a-z][a-z0-9+.-]*://\S+"
|
|
44
|
+
MULTI_SEGMENT_PATH_PATTERN: str = r"(?<![\w-])[\w.{}-]*/[\w.{}/-]*/[\w.{}-]+"
|
|
45
|
+
BLANKED_SPAN: str = " "
|
|
44
46
|
FENCE_OPEN_PATTERN: str = r"^```([A-Za-z0-9_-]*)\s*$"
|
|
45
47
|
FENCE_CLOSE_PATTERN: str = r"^```\s*$"
|
|
46
|
-
|
|
48
|
+
RETIRED_REASON_PREFIX: str = "retired_capability:"
|
|
47
49
|
|
|
48
50
|
UTF8_ENCODING: str = "utf-8"
|
|
49
51
|
NEWLINE_JOIN_SEPARATOR: str = "\n"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Constants for the banned-prose-word detector.
|
|
2
|
+
|
|
3
|
+
The package AGENTS.md bans a family of emphasis words from every authored
|
|
4
|
+
surface. These patterns describe where the ban applies and which spans of a
|
|
5
|
+
document are code rather than prose.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
UTF8_ENCODING: str = "utf-8"
|
|
13
|
+
|
|
14
|
+
ALL_GOVERNED_PATH_SEGMENTS: tuple[str, ...] = (
|
|
15
|
+
"/.agents/agents/",
|
|
16
|
+
"/.agents/skills/",
|
|
17
|
+
"/audit-rubrics/",
|
|
18
|
+
"/commands/",
|
|
19
|
+
"/docs/",
|
|
20
|
+
"/output-styles/",
|
|
21
|
+
"/rules/",
|
|
22
|
+
"/system-prompts/",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
ALL_EXEMPT_PATH_SEGMENTS: tuple[str, ...] = (
|
|
26
|
+
"-archived/",
|
|
27
|
+
"/fixtures/",
|
|
28
|
+
"/tests/",
|
|
29
|
+
"/changelog.md",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
ALL_GOVERNED_SUFFIXES: frozenset[str] = frozenset({".md", ".mdx", ".xml"})
|
|
33
|
+
|
|
34
|
+
ALL_BANNED_PROSE_WORDS: tuple[str, ...] = (
|
|
35
|
+
"real",
|
|
36
|
+
"really",
|
|
37
|
+
"real-world",
|
|
38
|
+
"actual",
|
|
39
|
+
"actually",
|
|
40
|
+
"genuine",
|
|
41
|
+
"genuinely",
|
|
42
|
+
"truly",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
BANNED_PROSE_WORD_PATTERN: re.Pattern[str] = re.compile(
|
|
46
|
+
r"(?<![\w-])(" + "|".join(ALL_BANNED_PROSE_WORDS) + r")(?![\w-])",
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
ADJECTIVAL_TRUE_PATTERN: re.Pattern[str] = re.compile(
|
|
51
|
+
r"(?<![\w-])true(?![\w-])\s+[a-z]", re.IGNORECASE
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
ALL_PREDICATIVE_TRUE_LEADS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"is",
|
|
57
|
+
"are",
|
|
58
|
+
"was",
|
|
59
|
+
"were",
|
|
60
|
+
"be",
|
|
61
|
+
"been",
|
|
62
|
+
"being",
|
|
63
|
+
"always",
|
|
64
|
+
"still",
|
|
65
|
+
"holds",
|
|
66
|
+
"hold",
|
|
67
|
+
"held",
|
|
68
|
+
"stays",
|
|
69
|
+
"stay",
|
|
70
|
+
"remains",
|
|
71
|
+
"remain",
|
|
72
|
+
"returns",
|
|
73
|
+
"return",
|
|
74
|
+
"reads",
|
|
75
|
+
"read",
|
|
76
|
+
"and",
|
|
77
|
+
"or",
|
|
78
|
+
"not",
|
|
79
|
+
"never",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
FENCED_CODE_PATTERN: re.Pattern[str] = re.compile(r"^\s*```")
|
|
84
|
+
INDENTED_CODE_PATTERN: re.Pattern[str] = re.compile(r"^(?: {4,}|\t)\S")
|
|
85
|
+
INLINE_CODE_PATTERN: re.Pattern[str] = re.compile(r"`[^`]*`")
|
|
86
|
+
URL_PATTERN: re.Pattern[str] = re.compile(r"\b(?:https?|file)://\S+")
|
|
87
|
+
PATH_PATTERN: re.Pattern[str] = re.compile(r"(?<![\w])[\w./-]*/[\w./-]+")
|
|
88
|
+
MARKDOWN_LINK_TARGET_PATTERN: re.Pattern[str] = re.compile(r"\]\([^)]*\)")
|
|
89
|
+
XML_TAG_PATTERN: re.Pattern[str] = re.compile(r"<[^>]+>")
|
|
90
|
+
BLANKED_SPAN: str = " "
|
|
91
|
+
|
|
92
|
+
BANNED_PROSE_WORD_RULE_ID: str = "banned-prose-word"
|
|
93
|
+
BANNED_PROSE_WORD_MESSAGE: str = (
|
|
94
|
+
"Banned word: {word}. Delete it and name the evidence instead."
|
|
95
|
+
)
|
|
@@ -85,14 +85,14 @@ ROLE_BUGTEAM: str = "bugteam"
|
|
|
85
85
|
"""Role name whose agent definition set the preflight validates by default."""
|
|
86
86
|
|
|
87
87
|
ROLE_CLEAN_CODER: str = "clean-coder"
|
|
88
|
-
"""
|
|
88
|
+
"""Compatibility role for FIX and standards-fix using poteto-agent."""
|
|
89
89
|
|
|
90
90
|
DEFAULT_ROLE: str = ROLE_BUGTEAM
|
|
91
91
|
"""Role applied when the caller does not pass ``--role``."""
|
|
92
92
|
|
|
93
93
|
ALL_AGENT_FILENAMES_BY_ROLE: dict[str, tuple[str, ...]] = {
|
|
94
|
-
ROLE_BUGTEAM: ("
|
|
95
|
-
ROLE_CLEAN_CODER: ("
|
|
94
|
+
ROLE_BUGTEAM: ("poteto-agent.md",),
|
|
95
|
+
ROLE_CLEAN_CODER: ("poteto-agent.md",),
|
|
96
96
|
}
|
|
97
97
|
"""Agent definition filenames required under ``agents/`` for each known role."""
|
|
98
98
|
|
|
@@ -19,7 +19,7 @@ Import ``run_headless_worker`` for the outcome object::
|
|
|
19
19
|
working_directory=cwd,
|
|
20
20
|
run_state_directory=run_dir,
|
|
21
21
|
timeout_seconds=5400,
|
|
22
|
-
agent_name="
|
|
22
|
+
agent_name="poteto-agent",
|
|
23
23
|
)
|
|
24
24
|
"""
|
|
25
25
|
|
|
@@ -72,6 +72,7 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
72
72
|
WORKER_SPEC_TIMEOUT_KEY,
|
|
73
73
|
)
|
|
74
74
|
from shared_tree_paths import resolve_shared_process_tree_scripts_directory
|
|
75
|
+
from grok_worker_preflight import poteto_mode_skill_path
|
|
75
76
|
|
|
76
77
|
_shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
|
|
77
78
|
__file__,
|
|
@@ -420,6 +421,23 @@ def run_headless_worker(
|
|
|
420
421
|
if leader_socket_path is not None
|
|
421
422
|
else _mint_leader_socket_path(run_state_directory)
|
|
422
423
|
)
|
|
424
|
+
if agent_name == "poteto-agent":
|
|
425
|
+
try:
|
|
426
|
+
skill_path = poteto_mode_skill_path().resolve()
|
|
427
|
+
skill_text = skill_path.read_text(encoding=UTF8_ENCODING)
|
|
428
|
+
task_text = prompt_file.read_text(encoding=UTF8_ENCODING)
|
|
429
|
+
run_state_directory.mkdir(parents=True, exist_ok=True)
|
|
430
|
+
prompt_file = run_state_directory / f"{resolved_leader_socket_path.stem}-poteto.txt"
|
|
431
|
+
prompt_file.write_text(
|
|
432
|
+
f"Poteto skill source: {skill_path}\n"
|
|
433
|
+
"Resolve companion skill paths against that source directory.\n"
|
|
434
|
+
"The task below defines your review, build, or translation duty. "
|
|
435
|
+
"Preserve its scope, write permissions, and report contract.\n\n"
|
|
436
|
+
f"{skill_text}\n\nTask:\n{task_text}",
|
|
437
|
+
encoding=UTF8_ENCODING,
|
|
438
|
+
)
|
|
439
|
+
except OSError as error:
|
|
440
|
+
return _launch_failure_outcome(error)
|
|
423
441
|
all_arguments = _build_invocation(
|
|
424
442
|
prompt_file=prompt_file,
|
|
425
443
|
working_directory=working_directory,
|
|
@@ -45,6 +45,7 @@ from __future__ import annotations
|
|
|
45
45
|
|
|
46
46
|
import argparse
|
|
47
47
|
import json
|
|
48
|
+
import os
|
|
48
49
|
import shutil
|
|
49
50
|
import subprocess
|
|
50
51
|
import sys
|
|
@@ -61,6 +62,7 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
61
62
|
CLAUDE_HOME_SUBDIRECTORY,
|
|
62
63
|
CLI_PING_FLAG,
|
|
63
64
|
CLI_ROLE_FLAG,
|
|
65
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
64
66
|
CLI_RUN_STATE_DIR_FLAG,
|
|
65
67
|
DEFAULT_AUTH_TIMEOUT_SECONDS,
|
|
66
68
|
DEFAULT_PING_TIMEOUT_SECONDS,
|
|
@@ -111,7 +113,67 @@ class PreflightOutcome:
|
|
|
111
113
|
|
|
112
114
|
def claude_config_home() -> Path:
|
|
113
115
|
"""Return the per-user Claude configuration directory."""
|
|
114
|
-
|
|
116
|
+
configured_root = os.environ.get(CLAUDE_CONFIG_DIR_ENV_VAR)
|
|
117
|
+
return Path(configured_root) if configured_root else Path.home() / CLAUDE_HOME_SUBDIRECTORY
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _read_pstack_registry(registry_path: Path) -> dict[str, object]:
|
|
121
|
+
try:
|
|
122
|
+
registry = json.loads(registry_path.read_text(encoding=UTF8_ENCODING))
|
|
123
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
124
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: {error}") from error
|
|
125
|
+
if not isinstance(registry, dict):
|
|
126
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: root must be an object")
|
|
127
|
+
return registry
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _user_pstack_entries(all_registry: dict[str, object], registry_path: Path) -> list[dict[str, object]]:
|
|
131
|
+
plugins = all_registry.get("plugins")
|
|
132
|
+
if not isinstance(plugins, dict):
|
|
133
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: plugins must be an object")
|
|
134
|
+
entries = plugins.get("pstack@pstack-claude", [])
|
|
135
|
+
if not isinstance(entries, list) or any(not isinstance(entry, dict) for entry in entries):
|
|
136
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: pstack entries must be objects")
|
|
137
|
+
user_entries = [entry for entry in entries if entry.get("scope") == "user"]
|
|
138
|
+
if len(user_entries) > 1:
|
|
139
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: multiple user pstack installations")
|
|
140
|
+
return user_entries
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _native_pstack_skill_path(registry_path: Path, fallback_path: Path) -> Path:
|
|
144
|
+
if not registry_path.exists():
|
|
145
|
+
return fallback_path
|
|
146
|
+
user_entries = _user_pstack_entries(_read_pstack_registry(registry_path), registry_path)
|
|
147
|
+
if not user_entries:
|
|
148
|
+
return fallback_path
|
|
149
|
+
install_path = user_entries[0].get("installPath")
|
|
150
|
+
if not isinstance(install_path, str) or not Path(install_path).is_absolute():
|
|
151
|
+
raise OSError(f"Invalid pstack installation registry {registry_path}: pstack installPath must be absolute")
|
|
152
|
+
return Path(install_path) / "skills" / "poteto-mode" / "SKILL.md"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def poteto_mode_skill_path() -> Path:
|
|
156
|
+
"""Return the selected installed pstack poteto-mode skill path.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The validated poteto-mode skill path.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
OSError: If the registry or selected skill is invalid.
|
|
163
|
+
"""
|
|
164
|
+
config_root = claude_config_home()
|
|
165
|
+
fallback_path = config_root / "skills" / "poteto-mode" / "SKILL.md"
|
|
166
|
+
skill_path = _native_pstack_skill_path(
|
|
167
|
+
config_root / "plugins" / "installed_plugins.json",
|
|
168
|
+
fallback_path,
|
|
169
|
+
)
|
|
170
|
+
try:
|
|
171
|
+
skill_text = skill_path.read_text(encoding=UTF8_ENCODING)
|
|
172
|
+
except UnicodeError as error:
|
|
173
|
+
raise OSError(f"Invalid UTF-8 poteto skill {skill_path}") from error
|
|
174
|
+
if not skill_text.strip():
|
|
175
|
+
raise OSError(f"Empty poteto skill {skill_path}")
|
|
176
|
+
return skill_path
|
|
115
177
|
|
|
116
178
|
|
|
117
179
|
def _install_manifest_path() -> Path:
|
|
@@ -218,6 +280,10 @@ def _is_grok_binary_resolvable() -> bool:
|
|
|
218
280
|
def _is_claude_dev_env_config_present(role: str) -> bool:
|
|
219
281
|
if not _install_manifest_path().is_file():
|
|
220
282
|
return False
|
|
283
|
+
try:
|
|
284
|
+
poteto_mode_skill_path()
|
|
285
|
+
except OSError:
|
|
286
|
+
return False
|
|
221
287
|
all_agent_filenames = ALL_AGENT_FILENAMES_BY_ROLE.get(role)
|
|
222
288
|
if all_agent_filenames is None:
|
|
223
289
|
return False
|
|
@@ -15,23 +15,33 @@ from .config import constants
|
|
|
15
15
|
from .model import Diagnostic, Document, Location, Severity
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
"""Return whether the document is
|
|
18
|
+
def accepts_instruction_markdown(document: Document) -> bool:
|
|
19
|
+
"""Return whether the document is authored instruction Markdown.
|
|
20
|
+
|
|
21
|
+
Rules, skills, agents, commands, documentation and stored system prompts
|
|
22
|
+
all describe hooks to a reader, so each one carries the same claim.
|
|
20
23
|
|
|
21
24
|
Args:
|
|
22
25
|
document: Candidate document.
|
|
23
26
|
|
|
24
27
|
Returns:
|
|
25
|
-
True for
|
|
28
|
+
True for Markdown on an instruction surface.
|
|
26
29
|
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
if document.path.suffix.lower() not in constants.ALL_MARKDOWN_SUFFIXES:
|
|
31
|
+
return False
|
|
32
|
+
normalized_path = f"/{document.path.as_posix().lower()}"
|
|
33
|
+
return any(
|
|
34
|
+
each_segment in normalized_path
|
|
35
|
+
for each_segment in constants.ALL_HOOK_PROSE_SURFACE_SEGMENTS
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
def _package_root(repository_root: Path, document: Document) -> Path:
|
|
34
|
-
|
|
40
|
+
for each_ancestor in document.path.parents:
|
|
41
|
+
candidate_root = repository_root / each_ancestor.as_posix()
|
|
42
|
+
if (candidate_root / constants.HOOKS_DIRECTORY_NAME).is_dir():
|
|
43
|
+
return candidate_root
|
|
44
|
+
return repository_root / document.path.parent.as_posix()
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
def _read_text(path: Path) -> str:
|