@event4u/agent-config 1.25.0 → 1.27.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/e2e-heal.md +2 -0
- package/.agent-src/commands/e2e-plan.md +2 -0
- package/.agent-src/rules/domain-adoption-policy.md +158 -0
- package/.agent-src/rules/no-unsolicited-rebase.md +107 -0
- package/.agent-src/skills/mobile-e2e-strategy/SKILL.md +147 -0
- package/.agent-src/skills/playwright-testing/SKILL.md +1 -0
- package/.agent-src/skills/react-native-setup/SKILL.md +221 -0
- package/.claude-plugin/marketplace.json +3 -1
- package/CHANGELOG.md +32 -0
- package/README.md +2 -2
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +9 -4
- package/docs/contracts/linter-structural-model.md +180 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/ios-simulator-guide.md +383 -0
- package/docs/guidelines/agent-infra/size-and-scope.md +18 -12
- package/package.json +1 -1
- package/scripts/measure_density.py +232 -0
- package/scripts/skill_linter.py +156 -27
package/scripts/skill_linter.py
CHANGED
|
@@ -264,9 +264,9 @@ def _count_code_blocks(text: str) -> int:
|
|
|
264
264
|
def _fenced_content_ratio(text: str) -> float:
|
|
265
265
|
"""Return the fraction of non-empty lines that sit inside fenced blocks.
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
Retained as a helper for backwards compatibility; the size gates use
|
|
268
|
+
:func:`_density_score` from the structural model instead (Phase 3 of
|
|
269
|
+
road-to-structural-linter-reform).
|
|
270
270
|
"""
|
|
271
271
|
inside = False
|
|
272
272
|
fenced_lines = 0
|
|
@@ -287,6 +287,106 @@ def _fenced_content_ratio(text: str) -> float:
|
|
|
287
287
|
return fenced_lines / non_empty
|
|
288
288
|
|
|
289
289
|
|
|
290
|
+
# --- Structural-density model (docs/contracts/linter-structural-model.md) ---
|
|
291
|
+
# Replaces the raw line/word/fenced-ratio gates with four primitives that
|
|
292
|
+
# distinguish complexity from bloat. Calibrated 2026-05-08 against the full
|
|
293
|
+
# 310-artefact corpus (agents/.density-snapshot.jsonl).
|
|
294
|
+
|
|
295
|
+
PROCEDURE_HEADING_PATTERN = re.compile(
|
|
296
|
+
r"^##\s+Procedure(\s*[:\u2014\-].*)?\s*$", re.MULTILINE
|
|
297
|
+
)
|
|
298
|
+
COMMAND_FRONTMATTER_DELEGATION_KEYS = ("cluster:", "routes_to:")
|
|
299
|
+
MD_LINK_PATTERN = re.compile(r"\[[^\]]+\]\(([^)]+\.md[^)]*)\)")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _density_score(text: str) -> float:
|
|
303
|
+
"""Return structural density 0.0–1.0 — see docs/contracts/linter-structural-model.md.
|
|
304
|
+
|
|
305
|
+
density = structured_lines / non_blank_lines, where structured_lines =
|
|
306
|
+
fenced + table + bullet + numbered + heading. Higher = more structured
|
|
307
|
+
(catalogue, table, code, list); lower = prose-dominant.
|
|
308
|
+
"""
|
|
309
|
+
inside_fence = False
|
|
310
|
+
structured = 0
|
|
311
|
+
non_blank = 0
|
|
312
|
+
for raw in text.splitlines():
|
|
313
|
+
stripped = raw.strip()
|
|
314
|
+
if not stripped:
|
|
315
|
+
continue
|
|
316
|
+
non_blank += 1
|
|
317
|
+
if stripped.startswith("```"):
|
|
318
|
+
inside_fence = not inside_fence
|
|
319
|
+
structured += 1
|
|
320
|
+
continue
|
|
321
|
+
if inside_fence:
|
|
322
|
+
structured += 1
|
|
323
|
+
continue
|
|
324
|
+
if stripped.startswith("#"):
|
|
325
|
+
structured += 1
|
|
326
|
+
continue
|
|
327
|
+
if stripped.startswith("|") and stripped.endswith("|"):
|
|
328
|
+
structured += 1
|
|
329
|
+
continue
|
|
330
|
+
if stripped.startswith(("- ", "* ", "+ ")):
|
|
331
|
+
structured += 1
|
|
332
|
+
continue
|
|
333
|
+
if re.match(r"^\d+\.\s", stripped):
|
|
334
|
+
structured += 1
|
|
335
|
+
continue
|
|
336
|
+
if non_blank == 0:
|
|
337
|
+
return 0.0
|
|
338
|
+
return round(structured / non_blank, 3)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _count_procedure_sections(text: str) -> int:
|
|
342
|
+
"""Count `## Procedure` (or `## Procedure: <name>`) blocks in *text*."""
|
|
343
|
+
return len(PROCEDURE_HEADING_PATTERN.findall(text))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _command_delegation_signal(text: str, frontmatter: Optional[str]) -> bool:
|
|
347
|
+
"""Return True when a command has a delegation signal.
|
|
348
|
+
|
|
349
|
+
Signals: frontmatter declares ``cluster:`` or ``routes_to:`` — OR — the
|
|
350
|
+
body contains ≥ 3 markdown links to other ``.md`` files. Either signal
|
|
351
|
+
is sufficient (council review 2026-05-08).
|
|
352
|
+
"""
|
|
353
|
+
if frontmatter:
|
|
354
|
+
for key in COMMAND_FRONTMATTER_DELEGATION_KEYS:
|
|
355
|
+
if re.search(rf"^{re.escape(key)}", frontmatter, re.MULTILINE):
|
|
356
|
+
return True
|
|
357
|
+
if len(MD_LINK_PATTERN.findall(text)) >= 3:
|
|
358
|
+
return True
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _iron_law_blocks(text: str) -> int:
|
|
363
|
+
"""Count fenced blocks that look like verbatim Iron-Law imperatives.
|
|
364
|
+
|
|
365
|
+
Heuristic: fenced block whose body has ≥ 30 alphabetical chars and
|
|
366
|
+
≥ 60 % uppercase across ≥ 1 non-empty line. The 30-char floor filters
|
|
367
|
+
short ALL-CAPS markers (``OK``, ``WIP``); the 60 %-uppercase floor
|
|
368
|
+
catches verbatim imperatives (``NEVER COMMIT.``).
|
|
369
|
+
"""
|
|
370
|
+
blocks = 0
|
|
371
|
+
inside = False
|
|
372
|
+
body: list[str] = []
|
|
373
|
+
for raw in text.splitlines():
|
|
374
|
+
if raw.strip().startswith("```"):
|
|
375
|
+
if inside and body:
|
|
376
|
+
non_empty = [b for b in body if b.strip()]
|
|
377
|
+
letters = "".join(non_empty)
|
|
378
|
+
upper = sum(1 for c in letters if c.isalpha() and c.isupper())
|
|
379
|
+
total = sum(1 for c in letters if c.isalpha())
|
|
380
|
+
if total >= 30 and upper / total >= 0.6 and non_empty:
|
|
381
|
+
blocks += 1
|
|
382
|
+
inside = not inside
|
|
383
|
+
body = []
|
|
384
|
+
continue
|
|
385
|
+
if inside:
|
|
386
|
+
body.append(raw)
|
|
387
|
+
return blocks
|
|
388
|
+
|
|
389
|
+
|
|
290
390
|
def extract_description(text: str) -> Optional[str]:
|
|
291
391
|
frontmatter = FRONTMATTER_PATTERN.search(text)
|
|
292
392
|
if not frontmatter:
|
|
@@ -561,14 +661,28 @@ def lint_skill(path: Path, text: str) -> LintResult:
|
|
|
561
661
|
"Assisted skill has no validation/challenge step in procedure"))
|
|
562
662
|
suggestions.append("Add a requirement-checking or validation step before implementation")
|
|
563
663
|
|
|
564
|
-
# --- Size check (
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
664
|
+
# --- Size check (docs/contracts/linter-structural-model.md) ---
|
|
665
|
+
# Structural-density gate replaces raw line count (Phase 3 of
|
|
666
|
+
# road-to-structural-linter-reform, 2026-05-08): warn only when the skill
|
|
667
|
+
# is *both* large AND prose-dominant OR ships ≥ 2 independently invocable
|
|
668
|
+
# procedures. Reference catalogues (quality-tools 411 L / density 0.83)
|
|
669
|
+
# pass; multi-procedure skills are flagged for split.
|
|
569
670
|
total_lines = len(text.splitlines())
|
|
570
671
|
if total_lines > 400:
|
|
571
|
-
|
|
672
|
+
density = _density_score(text)
|
|
673
|
+
procedures = _count_procedure_sections(text)
|
|
674
|
+
if density < 0.6 or procedures >= 2:
|
|
675
|
+
reason = (
|
|
676
|
+
f"density {density:.2f} < 0.60"
|
|
677
|
+
if density < 0.6
|
|
678
|
+
else f"{procedures} ## Procedure blocks (≥ 2)"
|
|
679
|
+
)
|
|
680
|
+
issues.append(Issue(
|
|
681
|
+
"warning",
|
|
682
|
+
"skill_too_large",
|
|
683
|
+
f"Skill has {total_lines} lines and {reason}; review for split "
|
|
684
|
+
f"(see linter-structural-model contract)",
|
|
685
|
+
))
|
|
572
686
|
|
|
573
687
|
# --- Pointer-only / guideline-dependent skill detection ---
|
|
574
688
|
if procedure_block:
|
|
@@ -1021,19 +1135,26 @@ def lint_rule(path: Path, text: str) -> LintResult:
|
|
|
1021
1135
|
if DOUBLE_BLANK_PATTERN.search(text):
|
|
1022
1136
|
issues.append(Issue("warning", "double_blank_lines", "File contains double or triple blank lines"))
|
|
1023
1137
|
|
|
1024
|
-
# --- Content checks (
|
|
1025
|
-
#
|
|
1026
|
-
#
|
|
1027
|
-
#
|
|
1138
|
+
# --- Content checks (docs/contracts/linter-structural-model.md) ---
|
|
1139
|
+
# Structural-density gate replaces fenced-ratio + dual-threshold (Phase 3
|
|
1140
|
+
# of road-to-structural-linter-reform, 2026-05-08): warn only when the
|
|
1141
|
+
# rule is long, prose-dominant, AND ships no Iron-Law block. Hard error
|
|
1142
|
+
# at 200 lines stays unconditional.
|
|
1028
1143
|
line_count = len([line for line in text.splitlines() if line.strip()])
|
|
1029
1144
|
total_lines = len(text.splitlines())
|
|
1030
|
-
fenced_ratio = _fenced_content_ratio(text)
|
|
1031
1145
|
if total_lines > 200:
|
|
1032
1146
|
issues.append(Issue("error", "rule_too_large", f"Rule has {total_lines} lines (hard limit: 200); must split or move to guideline"))
|
|
1033
|
-
elif line_count > 60
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1147
|
+
elif line_count > 60:
|
|
1148
|
+
density = _density_score(text)
|
|
1149
|
+
iron_blocks = _iron_law_blocks(text)
|
|
1150
|
+
if density < 0.5 and iron_blocks == 0:
|
|
1151
|
+
issues.append(Issue(
|
|
1152
|
+
"warning",
|
|
1153
|
+
"long_rule",
|
|
1154
|
+
f"Rule has {line_count} non-empty lines, density {density:.2f} < 0.50, "
|
|
1155
|
+
f"no Iron-Law block; rules should be concise "
|
|
1156
|
+
f"(see linter-structural-model contract)",
|
|
1157
|
+
))
|
|
1037
1158
|
|
|
1038
1159
|
for bad_sign in RULE_BAD_SIGNS:
|
|
1039
1160
|
if bad_sign in text:
|
|
@@ -1177,17 +1298,25 @@ def lint_command(path: Path, text: str) -> LintResult:
|
|
|
1177
1298
|
if not has_steps and not has_numbered:
|
|
1178
1299
|
issues.append(Issue("warning", "no_steps", "Command has no Steps section or numbered sub-headings"))
|
|
1179
1300
|
|
|
1180
|
-
# --- Size check (
|
|
1181
|
-
#
|
|
1182
|
-
# 2026-05-
|
|
1183
|
-
#
|
|
1301
|
+
# --- Size check (docs/contracts/linter-structural-model.md) ---
|
|
1302
|
+
# Structural-density gate replaces sub-section + code-block heuristic
|
|
1303
|
+
# (Phase 3 of road-to-structural-linter-reform, 2026-05-08): warn only
|
|
1304
|
+
# when the command is large, lacks a delegation signal (frontmatter
|
|
1305
|
+
# cluster:/routes_to: OR ≥ 3 markdown links to other .md files), AND
|
|
1306
|
+
# has density < 0.65.
|
|
1184
1307
|
word_count = len(text.split())
|
|
1185
1308
|
if word_count > 1000:
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1309
|
+
density = _density_score(text)
|
|
1310
|
+
delegated = _command_delegation_signal(text, frontmatter)
|
|
1311
|
+
if not delegated and density < 0.65:
|
|
1312
|
+
issues.append(Issue(
|
|
1313
|
+
"warning",
|
|
1314
|
+
"large_command",
|
|
1315
|
+
f"Command has {word_count} words, density {density:.2f} < 0.65, "
|
|
1316
|
+
f"no delegation signal (frontmatter cluster:/routes_to: or "
|
|
1317
|
+
f"≥ 3 .md links); review for split or delegation "
|
|
1318
|
+
f"(see linter-structural-model contract)",
|
|
1319
|
+
))
|
|
1191
1320
|
|
|
1192
1321
|
# File must end with exactly one newline
|
|
1193
1322
|
if not text.endswith("\n"):
|