@event4u/agent-config 1.12.0 ā 1.14.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/.agent-src/commands/agent-handoff.md +3 -0
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +5 -1
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +5 -0
- package/.agent-src/commands/chat-history-resume.md +5 -0
- package/.agent-src/commands/chat-history.md +5 -0
- package/.agent-src/commands/check-current-md.md +126 -0
- package/.agent-src/commands/commit-in-chunks.md +98 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +3 -0
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +3 -0
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +24 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +3 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +4 -1
- package/.agent-src/commands/review-changes.md +4 -0
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +7 -0
- package/.agent-src/commands/roadmap-execute.md +12 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +3 -0
- package/.agent-src/commands/sync-agent-settings.md +3 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +4 -0
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +8 -0
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history.md +147 -118
- package/.agent-src/rules/cli-output-handling.md +26 -3
- package/.agent-src/rules/command-suggestion.md +133 -0
- package/.agent-src/rules/commit-policy.md +99 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +81 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +103 -30
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +1 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +81 -3
- package/.agent-src/scripts/update_roadmap_progress.py +48 -6
- package/.agent-src/skills/blade-ui/SKILL.md +30 -5
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +41 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
- package/.agent-src/skills/fe-design/SKILL.md +72 -60
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +30 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
- package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +1 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/github-workflows/roadmap-progress-check.yml +63 -0
- package/.agent-src/templates/hooks/pre-commit-roadmap-progress +60 -0
- package/.agent-src/templates/roadmaps.md +8 -2
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/memory_lookup.py +382 -21
- package/.agent-src/templates/scripts/memory_status.py +110 -9
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
- package/.agent-src/templates/scripts/{implement_ticket ā work_engine}/delivery_state.py +7 -0
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/implement.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/memory.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/report.py +36 -4
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps ā work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +36 -8
- package/CHANGELOG.md +558 -0
- package/README.md +146 -4
- package/composer.json +3 -0
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/architecture.md +28 -1
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -2
- package/docs/installation.md +86 -0
- package/docs/showcase.md +204 -0
- package/package.json +9 -1
- package/scripts/agent-config +274 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +36 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/memory_lookup.py +143 -7
- package/scripts/memory_status.py +76 -14
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/postinstall.sh +16 -0
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +67 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
- /package/.agent-src/templates/scripts/{implement_ticket ā work_engine}/persona_policy.py +0 -0
|
@@ -8,6 +8,8 @@ Checks that compression preserved structural integrity:
|
|
|
8
8
|
- All code blocks preserved exactly
|
|
9
9
|
- YAML frontmatter identical
|
|
10
10
|
- Word count reduction within healthy range (10-60%)
|
|
11
|
+
- Iron Law sections (## Iron Law / ### Iron Law / ## The Iron Law / Iron Laws / numbered)
|
|
12
|
+
preserved per `preservation-guard`: heading verbatim at original level, ⤠15% reduction
|
|
11
13
|
|
|
12
14
|
Exit codes: 0 = clean, 1 = issues found, 3 = internal error
|
|
13
15
|
"""
|
|
@@ -60,6 +62,103 @@ def extract_frontmatter(text: str) -> str:
|
|
|
60
62
|
return m.group(1).strip() if m else ""
|
|
61
63
|
|
|
62
64
|
|
|
65
|
+
# Matches `## Iron Law`, `## The Iron Law`, `## Iron Laws`, `### Iron Law ā ā¦`,
|
|
66
|
+
# `## Iron Law 1 ā ā¦`, etc. Any heading level 2-6.
|
|
67
|
+
IRON_LAW_HEADING = re.compile(r"^(#{2,6})\s+(The\s+)?Iron Laws?\b")
|
|
68
|
+
|
|
69
|
+
LIST_ITEM_RE = re.compile(r"^(?:[-*+]|\d+\.)\s")
|
|
70
|
+
INNER_HEADING_RE = re.compile(r"^#{1,6}\s")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def count_iron_law_structure(body: str) -> dict:
|
|
74
|
+
"""Count structural units in an Iron Law body.
|
|
75
|
+
|
|
76
|
+
Returns counts of paragraphs (blank-line-separated prose blocks),
|
|
77
|
+
list items (bullet + numbered), and fenced code blocks. Caveman
|
|
78
|
+
compression may shorten word count freely; what must NOT change is
|
|
79
|
+
the count of these structural units. Each represents a passage of
|
|
80
|
+
the law that the source decided to keep.
|
|
81
|
+
|
|
82
|
+
Multi-line list items (bullet text wrapped to indented continuation
|
|
83
|
+
lines, no blank line between) count as ONE list item, not as a
|
|
84
|
+
list item plus a paragraph.
|
|
85
|
+
"""
|
|
86
|
+
paragraphs = 0
|
|
87
|
+
list_items = 0
|
|
88
|
+
code_blocks = 0
|
|
89
|
+
in_code = False
|
|
90
|
+
state = "blank" # "blank" | "paragraph" | "list"
|
|
91
|
+
for line in body.splitlines():
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
if stripped.startswith("```"):
|
|
94
|
+
if not in_code:
|
|
95
|
+
code_blocks += 1
|
|
96
|
+
in_code = not in_code
|
|
97
|
+
state = "blank"
|
|
98
|
+
continue
|
|
99
|
+
if in_code:
|
|
100
|
+
continue
|
|
101
|
+
if not stripped:
|
|
102
|
+
state = "blank"
|
|
103
|
+
continue
|
|
104
|
+
if LIST_ITEM_RE.match(stripped):
|
|
105
|
+
list_items += 1
|
|
106
|
+
state = "list"
|
|
107
|
+
continue
|
|
108
|
+
if INNER_HEADING_RE.match(stripped):
|
|
109
|
+
state = "blank"
|
|
110
|
+
continue
|
|
111
|
+
# Indented non-empty line right after a list item is a wrap
|
|
112
|
+
# continuation of that item, not a new paragraph.
|
|
113
|
+
if state == "list" and line.startswith((" ", "\t")):
|
|
114
|
+
continue
|
|
115
|
+
if state != "paragraph":
|
|
116
|
+
paragraphs += 1
|
|
117
|
+
state = "paragraph"
|
|
118
|
+
return {"paragraphs": paragraphs, "list_items": list_items, "code_blocks": code_blocks}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_iron_law_sections(text: str) -> list[tuple[str, int, str]]:
|
|
122
|
+
"""Return [(heading, level, body)] for each Iron Law section.
|
|
123
|
+
|
|
124
|
+
Body is everything after the heading until the next heading at the same
|
|
125
|
+
or higher (numerically lower) level ā fenced code blocks included verbatim.
|
|
126
|
+
"""
|
|
127
|
+
lines = text.splitlines()
|
|
128
|
+
sections: list[tuple[str, int, str]] = []
|
|
129
|
+
i = 0
|
|
130
|
+
in_code = False
|
|
131
|
+
while i < len(lines):
|
|
132
|
+
line = lines[i]
|
|
133
|
+
if line.strip().startswith("```"):
|
|
134
|
+
in_code = not in_code
|
|
135
|
+
i += 1
|
|
136
|
+
continue
|
|
137
|
+
if not in_code:
|
|
138
|
+
m = IRON_LAW_HEADING.match(line)
|
|
139
|
+
if m:
|
|
140
|
+
heading = line.rstrip()
|
|
141
|
+
level = len(m.group(1))
|
|
142
|
+
body_lines: list[str] = []
|
|
143
|
+
j = i + 1
|
|
144
|
+
inner_code = False
|
|
145
|
+
while j < len(lines):
|
|
146
|
+
jline = lines[j]
|
|
147
|
+
if jline.strip().startswith("```"):
|
|
148
|
+
inner_code = not inner_code
|
|
149
|
+
if not inner_code:
|
|
150
|
+
hm = re.match(r"^(#{1,6})\s", jline)
|
|
151
|
+
if hm and len(hm.group(1)) <= level:
|
|
152
|
+
break
|
|
153
|
+
body_lines.append(jline)
|
|
154
|
+
j += 1
|
|
155
|
+
sections.append((heading, level, "\n".join(body_lines)))
|
|
156
|
+
i = j
|
|
157
|
+
continue
|
|
158
|
+
i += 1
|
|
159
|
+
return sections
|
|
160
|
+
|
|
161
|
+
|
|
63
162
|
def check_pair(rel_path: str, source: str, compressed: str) -> List[Issue]:
|
|
64
163
|
"""Compare source and compressed versions of a file."""
|
|
65
164
|
issues: List[Issue] = []
|
|
@@ -96,6 +195,46 @@ def check_pair(rel_path: str, source: str, compressed: str) -> List[Issue]:
|
|
|
96
195
|
issues.append(Issue(rel_path, "modified_code_block", "error",
|
|
97
196
|
f"Code block {i+1} content changed during compression"))
|
|
98
197
|
|
|
198
|
+
# Iron Law preservation ā non-negotiable behavioral rules, see preservation-guard
|
|
199
|
+
src_laws = extract_iron_law_sections(source)
|
|
200
|
+
cmp_laws = extract_iron_law_sections(compressed)
|
|
201
|
+
cmp_law_map = {h: (lvl, body) for h, lvl, body in cmp_laws}
|
|
202
|
+
# Build a level-agnostic lookup so we can detect heading-level downgrades
|
|
203
|
+
# (`## Iron Law` ā `### Iron Law`).
|
|
204
|
+
cmp_law_by_text = {h.lstrip("# ").strip(): (lvl, h, body)
|
|
205
|
+
for h, lvl, body in cmp_laws}
|
|
206
|
+
for src_heading, src_level, src_body in src_laws:
|
|
207
|
+
src_text = src_heading.lstrip("# ").strip()
|
|
208
|
+
if src_heading not in cmp_law_map:
|
|
209
|
+
# Heading text may exist at a different level ā downgrade
|
|
210
|
+
if src_text in cmp_law_by_text:
|
|
211
|
+
cmp_level, cmp_heading, _ = cmp_law_by_text[src_text]
|
|
212
|
+
if cmp_level != src_level:
|
|
213
|
+
issues.append(Issue(rel_path, "iron_law_heading_downgrade", "error",
|
|
214
|
+
f"Iron Law heading level changed: "
|
|
215
|
+
f"{'#' * src_level} ā {'#' * cmp_level} "
|
|
216
|
+
f"({src_heading.strip()})"))
|
|
217
|
+
continue
|
|
218
|
+
issues.append(Issue(rel_path, "iron_law_missing", "error",
|
|
219
|
+
f"Iron Law section removed during compression: "
|
|
220
|
+
f"{src_heading.strip()}"))
|
|
221
|
+
continue
|
|
222
|
+
# Section exists at correct level ā check structural-unit survival.
|
|
223
|
+
# Caveman compression is fine (drop articles, terse phrasing); what
|
|
224
|
+
# must NOT change is the count of paragraphs, list items, and code
|
|
225
|
+
# blocks. Each is a passage the source kept on purpose.
|
|
226
|
+
_, cmp_body = cmp_law_map[src_heading]
|
|
227
|
+
src_struct = count_iron_law_structure(src_body)
|
|
228
|
+
cmp_struct = count_iron_law_structure(cmp_body)
|
|
229
|
+
for kind, src_n in src_struct.items():
|
|
230
|
+
cmp_n = cmp_struct[kind]
|
|
231
|
+
if cmp_n < src_n:
|
|
232
|
+
issues.append(Issue(rel_path, "iron_law_passage_dropped", "error",
|
|
233
|
+
f"Iron Law section dropped "
|
|
234
|
+
f"{src_n - cmp_n} {kind} "
|
|
235
|
+
f"({src_n} ā {cmp_n}): "
|
|
236
|
+
f"{src_heading.strip()}"))
|
|
237
|
+
|
|
99
238
|
# Word count ratio
|
|
100
239
|
src_words = len(source.split())
|
|
101
240
|
cmp_words = len(compressed.split())
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Iron Law prominence checker ā enforces that any rule file declaring an
|
|
4
|
+
"Iron Law" places it at the top of the file at H2 level.
|
|
5
|
+
|
|
6
|
+
Rules:
|
|
7
|
+
1. No heading at H3 or deeper may match "Iron Law(s)" ā Iron Laws must
|
|
8
|
+
be H2 sections, never sub-sections.
|
|
9
|
+
2. If a file declares one or more Iron-Law H2 sections, at least one
|
|
10
|
+
of them must be among the first two H2 headings of the file.
|
|
11
|
+
|
|
12
|
+
Files with no Iron-Law heading at all are exempt ā they may legitimately
|
|
13
|
+
reference Iron Laws from other rules in prose only.
|
|
14
|
+
|
|
15
|
+
Code blocks are skipped to avoid false positives on quoted text.
|
|
16
|
+
|
|
17
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from dataclasses import dataclass, asdict
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
|
|
30
|
+
IRON_LAW_RE = re.compile(r"\biron\s+laws?\b", re.IGNORECASE)
|
|
31
|
+
FENCE_RE = re.compile(r"^\s*```")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Violation:
|
|
36
|
+
file: str
|
|
37
|
+
line: int
|
|
38
|
+
kind: str # "deep_iron_law" | "buried_iron_law"
|
|
39
|
+
detail: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_headings(text: str) -> list[tuple[int, int, str]]:
|
|
43
|
+
"""Return (line_no, depth, title) for each heading outside code fences."""
|
|
44
|
+
headings: list[tuple[int, int, str]] = []
|
|
45
|
+
in_fence = False
|
|
46
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
47
|
+
if FENCE_RE.match(raw):
|
|
48
|
+
in_fence = not in_fence
|
|
49
|
+
continue
|
|
50
|
+
if in_fence:
|
|
51
|
+
continue
|
|
52
|
+
m = HEADING_RE.match(raw)
|
|
53
|
+
if not m:
|
|
54
|
+
continue
|
|
55
|
+
depth = len(m.group(1))
|
|
56
|
+
title = m.group(2).strip()
|
|
57
|
+
headings.append((lineno, depth, title))
|
|
58
|
+
return headings
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def scan_file(path: Path) -> list[Violation]:
|
|
62
|
+
text = path.read_text(encoding="utf-8")
|
|
63
|
+
headings = _parse_headings(text)
|
|
64
|
+
|
|
65
|
+
violations: list[Violation] = []
|
|
66
|
+
|
|
67
|
+
# Rule 1: no Iron Law at H3 or deeper
|
|
68
|
+
for lineno, depth, title in headings:
|
|
69
|
+
if depth >= 3 and IRON_LAW_RE.search(title):
|
|
70
|
+
violations.append(Violation(
|
|
71
|
+
file=str(path), line=lineno, kind="deep_iron_law",
|
|
72
|
+
detail=f"H{depth} heading `{title}` ā promote to H2",
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
# Rule 2: if any H2 Iron Law exists, it must be in first 2 H2 positions
|
|
76
|
+
h2 = [(ln, t) for ln, d, t in headings if d == 2]
|
|
77
|
+
iron_h2 = [(ln, t) for ln, t in h2 if IRON_LAW_RE.search(t)]
|
|
78
|
+
if iron_h2:
|
|
79
|
+
first_two_lines = {ln for ln, _ in h2[:2]}
|
|
80
|
+
if not any(ln in first_two_lines for ln, _ in iron_h2):
|
|
81
|
+
first_iron_ln, first_iron_title = iron_h2[0]
|
|
82
|
+
preceding = [t for ln, t in h2 if ln < first_iron_ln]
|
|
83
|
+
violations.append(Violation(
|
|
84
|
+
file=str(path), line=first_iron_ln, kind="buried_iron_law",
|
|
85
|
+
detail=(
|
|
86
|
+
f"Iron Law H2 `{first_iron_title}` at line {first_iron_ln} "
|
|
87
|
+
f"is preceded by {len(preceding)} non-Iron-Law H2 section(s): "
|
|
88
|
+
f"{preceding}. Move Iron Law into the first 2 H2 positions."
|
|
89
|
+
),
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
return violations
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_targets(paths: list[str]) -> list[Path]:
|
|
96
|
+
out: list[Path] = []
|
|
97
|
+
for raw in paths:
|
|
98
|
+
p = Path(raw)
|
|
99
|
+
if p.is_dir():
|
|
100
|
+
out.extend(sorted(p.rglob("*.md")))
|
|
101
|
+
elif p.suffix == ".md":
|
|
102
|
+
out.append(p)
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> int:
|
|
107
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"paths", nargs="*",
|
|
110
|
+
default=[".agent-src.uncompressed/rules"],
|
|
111
|
+
help="Files or directories to scan (default: .agent-src.uncompressed/rules)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
114
|
+
args = parser.parse_args()
|
|
115
|
+
|
|
116
|
+
targets = _resolve_targets(args.paths)
|
|
117
|
+
all_violations: list[Violation] = []
|
|
118
|
+
for path in targets:
|
|
119
|
+
if not path.exists():
|
|
120
|
+
print(f"ā ļø Not found: {path}", file=sys.stderr)
|
|
121
|
+
continue
|
|
122
|
+
all_violations.extend(scan_file(path))
|
|
123
|
+
|
|
124
|
+
if args.format == "json":
|
|
125
|
+
print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
|
|
126
|
+
else:
|
|
127
|
+
if not all_violations:
|
|
128
|
+
print(f"ā
Iron Law prominence clean ({len(targets)} file(s) scanned).")
|
|
129
|
+
else:
|
|
130
|
+
print(f"ā {len(all_violations)} Iron-Law prominence violation(s):\n")
|
|
131
|
+
for v in all_violations:
|
|
132
|
+
print(f" {v.file}:{v.line} ā {v.kind}")
|
|
133
|
+
print(f" ā {v.detail}")
|
|
134
|
+
|
|
135
|
+
return 1 if all_violations else 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
try:
|
|
140
|
+
sys.exit(main())
|
|
141
|
+
except Exception as exc: # noqa: BLE001
|
|
142
|
+
print(f"ā Internal error: {exc}", file=sys.stderr)
|
|
143
|
+
sys.exit(3)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Markdown language checker ā enforces language-and-tone § ".md files are ALWAYS English".
|
|
4
|
+
|
|
5
|
+
Scans .md files for German content (umlauts, function words, quoted DE phrases)
|
|
6
|
+
in body prose, skipping:
|
|
7
|
+
- Fenced code blocks (``` ... ```)
|
|
8
|
+
- Inline code (`...`)
|
|
9
|
+
- Labeled DE: ... Ā· EN: ... anchor blocks
|
|
10
|
+
- URLs and file paths inside backticks
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import dataclass, asdict
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Umlauts and German-only characters
|
|
25
|
+
UMLAUT_RE = re.compile(r"[äöüĆĆĆĆ]")
|
|
26
|
+
|
|
27
|
+
# German function words that almost never appear in English technical prose
|
|
28
|
+
DE_WORDS = [
|
|
29
|
+
"für", "nicht", "dass", "wenn", "sollte", "werden", "arbeite",
|
|
30
|
+
"selbststƤndig", "jetzt", "einfach", "weiter", "lƶsche", "frag",
|
|
31
|
+
"schreib", "mach", "auch", "hier", "diese", "dieser", "dieses",
|
|
32
|
+
"vermutlich", "bitte", "kannst", "sollen", "müssen", "wäre",
|
|
33
|
+
]
|
|
34
|
+
DE_WORD_RE = re.compile(
|
|
35
|
+
r"\b(" + "|".join(re.escape(w) for w in DE_WORDS) + r")\b",
|
|
36
|
+
re.IGNORECASE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Labeled bilingual anchor: lines starting with "DE:" or "- DE:" (and same for EN)
|
|
40
|
+
DE_ANCHOR_RE = re.compile(r"^\s*[-*]?\s*(DE|EN):\s", re.IGNORECASE)
|
|
41
|
+
|
|
42
|
+
# Inline code spans
|
|
43
|
+
INLINE_CODE_RE = re.compile(r"`[^`]*`")
|
|
44
|
+
|
|
45
|
+
# Per-line escape: append `<!-- md-language-check: ignore -->` to a line
|
|
46
|
+
# to suppress findings on that line. For meta-documentation that quotes
|
|
47
|
+
# German tokens as trigger examples (e.g. inside language-and-tone.md).
|
|
48
|
+
IGNORE_RE = re.compile(r"<!--\s*md-language-check:\s*ignore\s*-->", re.IGNORECASE)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Violation:
|
|
53
|
+
file: str
|
|
54
|
+
line: int
|
|
55
|
+
kind: str # "umlaut" | "de_word"
|
|
56
|
+
match: str
|
|
57
|
+
context: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _strip_inline_code(text: str) -> str:
|
|
61
|
+
return INLINE_CODE_RE.sub("", text)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def scan_file(path: Path) -> list[Violation]:
|
|
65
|
+
violations: list[Violation] = []
|
|
66
|
+
try:
|
|
67
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
68
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
69
|
+
print(f"ā ļø Cannot read {path}: {exc}", file=sys.stderr)
|
|
70
|
+
return violations
|
|
71
|
+
|
|
72
|
+
in_fence = False
|
|
73
|
+
in_frontmatter = False
|
|
74
|
+
for lineno, raw in enumerate(lines, start=1):
|
|
75
|
+
stripped = raw.lstrip()
|
|
76
|
+
|
|
77
|
+
# YAML frontmatter at top of file
|
|
78
|
+
if lineno == 1 and stripped == "---":
|
|
79
|
+
in_frontmatter = True
|
|
80
|
+
continue
|
|
81
|
+
if in_frontmatter:
|
|
82
|
+
if stripped == "---":
|
|
83
|
+
in_frontmatter = False
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Fenced code blocks
|
|
87
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
88
|
+
in_fence = not in_fence
|
|
89
|
+
continue
|
|
90
|
+
if in_fence:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Indented code blocks (4+ leading spaces, non-list)
|
|
94
|
+
if raw.startswith(" ") and not stripped.startswith(("-", "*", "+", "1", "2", "3", "4", "5", "6", "7", "8", "9")):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Labeled bilingual anchor ā allowed location for DE prose
|
|
98
|
+
if DE_ANCHOR_RE.match(raw):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Per-line opt-out marker
|
|
102
|
+
if IGNORE_RE.search(raw):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Strip inline code spans before scanning
|
|
106
|
+
scan_text = _strip_inline_code(raw)
|
|
107
|
+
|
|
108
|
+
for m in UMLAUT_RE.finditer(scan_text):
|
|
109
|
+
violations.append(Violation(
|
|
110
|
+
file=str(path), line=lineno, kind="umlaut",
|
|
111
|
+
match=m.group(0), context=raw.rstrip(),
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
for m in DE_WORD_RE.finditer(scan_text):
|
|
115
|
+
violations.append(Violation(
|
|
116
|
+
file=str(path), line=lineno, kind="de_word",
|
|
117
|
+
match=m.group(0), context=raw.rstrip(),
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
return violations
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> int:
|
|
124
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
125
|
+
parser.add_argument("paths", nargs="+", help="One or more .md files to scan")
|
|
126
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
all_violations: list[Violation] = []
|
|
130
|
+
for raw_path in args.paths:
|
|
131
|
+
path = Path(raw_path)
|
|
132
|
+
if not path.exists():
|
|
133
|
+
print(f"ā ļø Not found: {path}", file=sys.stderr)
|
|
134
|
+
continue
|
|
135
|
+
if path.suffix != ".md":
|
|
136
|
+
print(f"ā ļø Skipping non-.md: {path}", file=sys.stderr)
|
|
137
|
+
continue
|
|
138
|
+
all_violations.extend(scan_file(path))
|
|
139
|
+
|
|
140
|
+
if args.format == "json":
|
|
141
|
+
print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
|
|
142
|
+
else:
|
|
143
|
+
if not all_violations:
|
|
144
|
+
print("ā
No German content detected.")
|
|
145
|
+
else:
|
|
146
|
+
print(f"ā {len(all_violations)} violation(s) found:\n")
|
|
147
|
+
for v in all_violations:
|
|
148
|
+
print(f" {v.file}:{v.line} ā {v.kind} `{v.match}`")
|
|
149
|
+
print(f" ā {v.context}")
|
|
150
|
+
|
|
151
|
+
return 1 if all_violations else 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
try:
|
|
156
|
+
sys.exit(main())
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
print(f"ā Internal error: {exc}", file=sys.stderr)
|
|
159
|
+
sys.exit(3)
|
|
@@ -329,6 +329,42 @@ _CLI_INVOCATION_MAP: list[tuple[re.Pattern, str]] = [
|
|
|
329
329
|
re.compile(r"bash\s+scripts/first-run\.sh\b"),
|
|
330
330
|
"./agent-config first-run",
|
|
331
331
|
),
|
|
332
|
+
(
|
|
333
|
+
re.compile(r"(?:PYTHONPATH=\S+\s+)?python3\s+-m\s+work_engine\b"),
|
|
334
|
+
"./agent-config implement-ticket",
|
|
335
|
+
),
|
|
336
|
+
(
|
|
337
|
+
re.compile(r"(?:PYTHONPATH=\S+\s+)?python3\s+-m\s+implement_ticket\b"),
|
|
338
|
+
"./agent-config implement-ticket",
|
|
339
|
+
),
|
|
340
|
+
(
|
|
341
|
+
re.compile(r"python3\s+scripts/memory_lookup\.py\b"),
|
|
342
|
+
"./agent-config memory:lookup",
|
|
343
|
+
),
|
|
344
|
+
(
|
|
345
|
+
re.compile(r"python3\s+scripts/memory_signal\.py\b"),
|
|
346
|
+
"./agent-config memory:signal",
|
|
347
|
+
),
|
|
348
|
+
(
|
|
349
|
+
re.compile(r"python3\s+scripts/memory_hash\.py\b"),
|
|
350
|
+
"./agent-config memory:hash",
|
|
351
|
+
),
|
|
352
|
+
(
|
|
353
|
+
re.compile(r"python3\s+scripts/check_memory_proposal\.py\b"),
|
|
354
|
+
"./agent-config memory:check-proposal",
|
|
355
|
+
),
|
|
356
|
+
(
|
|
357
|
+
re.compile(r"python3\s+scripts/check_memory\.py\b"),
|
|
358
|
+
"./agent-config memory:check",
|
|
359
|
+
),
|
|
360
|
+
(
|
|
361
|
+
re.compile(r"python3\s+scripts/check_proposal\.py\b"),
|
|
362
|
+
"./agent-config proposal:check",
|
|
363
|
+
),
|
|
364
|
+
(
|
|
365
|
+
re.compile(r"python3\s+scripts/refine_ticket_detect\.py\b"),
|
|
366
|
+
"./agent-config refine-ticket:detect",
|
|
367
|
+
),
|
|
332
368
|
]
|
|
333
369
|
|
|
334
370
|
# Paths that legitimately document the raw invocations (e.g. the CLI's
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check_reply_consistency.py ā enforce user-interaction.md Iron Laws.
|
|
3
|
+
|
|
4
|
+
Single-Source Recommendation Line: a reply with numbered options must
|
|
5
|
+
have ONE bolded `Recommendation: N` / `Empfehlung: N` line, no inline
|
|
6
|
+
`(recommended)` / `(rec)` / `(empfohlen)` tag next to options, and the
|
|
7
|
+
recommended number must appear in the option block.
|
|
8
|
+
|
|
9
|
+
Modes:
|
|
10
|
+
--stdin / --file <path> Validate a single draft (all rules).
|
|
11
|
+
--scan-dir <dir> Scan .md tree for legacy inline-tag regression.
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 ok Ā· 2 inline tag Ā· 3 multi-rec Ā· 4 rec-not-in-options
|
|
15
|
+
5 options-without-rec (strict) Ā· 6 scan-dir found Ā· 9 usage error
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
OPTION_LINE_RE = re.compile(r"^\s*>?\s*(\d+)\.\s+\S")
|
|
25
|
+
REC_LINE_RE = re.compile(
|
|
26
|
+
r"(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b", re.IGNORECASE
|
|
27
|
+
)
|
|
28
|
+
TAG_RE = re.compile(r"\((?:recommended|rec|empfohlen)\)", re.IGNORECASE)
|
|
29
|
+
CODESPAN_RE = re.compile(r"`[^`\n]*`")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _strip_codespans(line: str) -> str:
|
|
33
|
+
return CODESPAN_RE.sub("``", line)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_inline_tag(text: str) -> tuple[int, str] | None:
|
|
37
|
+
"""Return (line_no, raw_line) of the first numbered-option line carrying
|
|
38
|
+
an inline (recommended)-class tag outside code spans, or None."""
|
|
39
|
+
for idx, raw in enumerate(text.splitlines(), start=1):
|
|
40
|
+
stripped = _strip_codespans(raw)
|
|
41
|
+
if not OPTION_LINE_RE.match(stripped):
|
|
42
|
+
continue
|
|
43
|
+
if TAG_RE.search(stripped):
|
|
44
|
+
return idx, raw.strip()
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_option_blocks(text: str) -> list[list[int]]:
|
|
49
|
+
"""Group consecutive numbered-option lines into blocks; return list of
|
|
50
|
+
blocks, each a list of the numbers found in that block."""
|
|
51
|
+
blocks: list[list[int]] = []
|
|
52
|
+
current: list[int] = []
|
|
53
|
+
for raw in text.splitlines():
|
|
54
|
+
m = OPTION_LINE_RE.match(raw)
|
|
55
|
+
if m:
|
|
56
|
+
current.append(int(m.group(1)))
|
|
57
|
+
else:
|
|
58
|
+
if len(current) >= 2:
|
|
59
|
+
blocks.append(current)
|
|
60
|
+
current = []
|
|
61
|
+
if len(current) >= 2:
|
|
62
|
+
blocks.append(current)
|
|
63
|
+
return blocks
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate(text: str, strict: bool = False) -> tuple[int, str]:
|
|
67
|
+
"""Run rules. Returns (exit_code, human_message)."""
|
|
68
|
+
tag = find_inline_tag(text)
|
|
69
|
+
if tag:
|
|
70
|
+
line_no, snippet = tag
|
|
71
|
+
return 2, f"line {line_no}: inline tag on numbered option ā {snippet!r}"
|
|
72
|
+
|
|
73
|
+
blocks = find_option_blocks(text)
|
|
74
|
+
rec_numbers = [int(n) for n in REC_LINE_RE.findall(text)]
|
|
75
|
+
|
|
76
|
+
if not blocks:
|
|
77
|
+
return 0, "ok (no numbered options block)"
|
|
78
|
+
|
|
79
|
+
if not rec_numbers:
|
|
80
|
+
if strict:
|
|
81
|
+
return 5, "numbered options without Recommendation:/Empfehlung: line"
|
|
82
|
+
return 0, "ok (options without recommendation; non-strict)"
|
|
83
|
+
|
|
84
|
+
distinct = sorted(set(rec_numbers))
|
|
85
|
+
if len(distinct) > 1:
|
|
86
|
+
return 3, f"multiple distinct recommendation numbers: {distinct}"
|
|
87
|
+
|
|
88
|
+
rec_num = distinct[0]
|
|
89
|
+
for block in blocks:
|
|
90
|
+
if rec_num in block:
|
|
91
|
+
return 0, f"ok (recommendation {rec_num} matches option block)"
|
|
92
|
+
return 4, f"recommendation {rec_num} not present in any option block"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_scan_dir(root: Path) -> int:
|
|
96
|
+
if not root.is_dir():
|
|
97
|
+
print(f"error: not a directory: {root}", file=sys.stderr)
|
|
98
|
+
return 9
|
|
99
|
+
violations: list[tuple[Path, int, str]] = []
|
|
100
|
+
for md in sorted(root.rglob("*.md")):
|
|
101
|
+
text = md.read_text(encoding="utf-8")
|
|
102
|
+
for idx, raw in enumerate(text.splitlines(), start=1):
|
|
103
|
+
stripped = _strip_codespans(raw)
|
|
104
|
+
if OPTION_LINE_RE.match(stripped) and TAG_RE.search(stripped):
|
|
105
|
+
violations.append((md, idx, raw.strip()))
|
|
106
|
+
if violations:
|
|
107
|
+
for path, line, snippet in violations:
|
|
108
|
+
print(f" š“ {path}:{line} ā inline-tag ā {snippet}", file=sys.stderr)
|
|
109
|
+
print(f"\nā {len(violations)} legacy-pattern violation(s)", file=sys.stderr)
|
|
110
|
+
return 6
|
|
111
|
+
print(f"ā
No legacy (recommended) tags found under {root}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main(argv: list[str] | None = None) -> int:
|
|
116
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
117
|
+
g = p.add_mutually_exclusive_group(required=True)
|
|
118
|
+
g.add_argument("--stdin", action="store_true", help="read draft from stdin")
|
|
119
|
+
g.add_argument("--file", type=Path, help="read draft from file")
|
|
120
|
+
g.add_argument("--scan-dir", type=Path, help="scan dir for legacy inline tags")
|
|
121
|
+
p.add_argument("--strict", action="store_true",
|
|
122
|
+
help="numbered options REQUIRE recommendation line (rule 5)")
|
|
123
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
124
|
+
args = p.parse_args(argv)
|
|
125
|
+
|
|
126
|
+
if args.scan_dir:
|
|
127
|
+
return cmd_scan_dir(args.scan_dir)
|
|
128
|
+
|
|
129
|
+
text = sys.stdin.read() if args.stdin else args.file.read_text(encoding="utf-8")
|
|
130
|
+
code, msg = validate(text, strict=args.strict)
|
|
131
|
+
if code == 0:
|
|
132
|
+
if args.verbose:
|
|
133
|
+
print(f"ā
{msg}")
|
|
134
|
+
return 0
|
|
135
|
+
print(f"ā [exit {code}] {msg}", file=sys.stderr)
|
|
136
|
+
return code
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Context-aware command suggestion engine.
|
|
2
|
+
|
|
3
|
+
Public API exposed for the always-on `command-suggestion` rule and for
|
|
4
|
+
tests. The engine is **deterministic** and **read-only**: it scores
|
|
5
|
+
candidate commands against a user message + recent context, applies
|
|
6
|
+
ranking, suppresses cooled-down suggestions, and renders a numbered
|
|
7
|
+
options block. It never executes a command ā the user pick is what
|
|
8
|
+
triggers the standard slash flow.
|
|
9
|
+
|
|
10
|
+
See `agents/contexts/command-suggestion-eligibility.md` for the
|
|
11
|
+
locked eligibility table and `road-to-context-aware-command-suggestion`
|
|
12
|
+
for the full design.
|
|
13
|
+
"""
|
|
14
|
+
from .types import CommandSpec, Match, Settings, CooldownState
|
|
15
|
+
from .loader import load_commands
|
|
16
|
+
from .match import match
|
|
17
|
+
from .rank import rank
|
|
18
|
+
from .cooldown import (
|
|
19
|
+
apply_cooldown,
|
|
20
|
+
CooldownStore,
|
|
21
|
+
detect_disable_directive,
|
|
22
|
+
is_explicit_slash_invocation,
|
|
23
|
+
)
|
|
24
|
+
from .render import render
|
|
25
|
+
from .sanitize import (
|
|
26
|
+
sanitize_context,
|
|
27
|
+
sanitize_message,
|
|
28
|
+
strip_code_blocks,
|
|
29
|
+
strip_suggestion_echo,
|
|
30
|
+
)
|
|
31
|
+
from .settings import load_settings
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CommandSpec",
|
|
35
|
+
"Match",
|
|
36
|
+
"Settings",
|
|
37
|
+
"CooldownState",
|
|
38
|
+
"CooldownStore",
|
|
39
|
+
"load_commands",
|
|
40
|
+
"load_settings",
|
|
41
|
+
"match",
|
|
42
|
+
"rank",
|
|
43
|
+
"apply_cooldown",
|
|
44
|
+
"detect_disable_directive",
|
|
45
|
+
"is_explicit_slash_invocation",
|
|
46
|
+
"render",
|
|
47
|
+
"sanitize_context",
|
|
48
|
+
"sanitize_message",
|
|
49
|
+
"strip_code_blocks",
|
|
50
|
+
"strip_suggestion_echo",
|
|
51
|
+
]
|