@tw93/waza 3.25.0 → 3.28.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.
Files changed (41) hide show
  1. package/README.md +49 -25
  2. package/package.json +5 -3
  3. package/rules/anti-patterns.md +24 -20
  4. package/rules/durable-context.md +6 -0
  5. package/rules/waza-routing.md +18 -0
  6. package/scripts/build_metadata.py +28 -16
  7. package/scripts/check_routing_drift.py +8 -0
  8. package/scripts/package-skill.sh +2 -3
  9. package/scripts/setup-rule.sh +4 -2
  10. package/scripts/setup-statusline.sh +1 -1
  11. package/scripts/skill_checks.py +290 -2
  12. package/scripts/statusline.sh +6 -14
  13. package/scripts/validate_package.py +1 -1
  14. package/scripts/verify_skills.py +12 -0
  15. package/skills/RESOLVER.md +8 -8
  16. package/skills/check/SKILL.md +78 -28
  17. package/skills/check/references/project-context.md +14 -6
  18. package/skills/check/scripts/audit_signals.py +192 -11
  19. package/skills/design/SKILL.md +39 -2
  20. package/skills/design/references/design-reference.md +17 -0
  21. package/skills/design/references/design-tokens.md +3 -11
  22. package/skills/health/SKILL.md +53 -26
  23. package/skills/health/agents/inspector-context.md +1 -1
  24. package/skills/health/scripts/check_agent_context.py +38 -1
  25. package/skills/health/scripts/check_maintainability.py +6 -0
  26. package/skills/health/scripts/collect-data.sh +11 -20
  27. package/skills/hunt/SKILL.md +33 -1
  28. package/skills/hunt/references/failure-patterns.md +54 -0
  29. package/skills/learn/SKILL.md +13 -3
  30. package/skills/read/SKILL.md +40 -9
  31. package/skills/read/references/read-methods.md +23 -4
  32. package/skills/read/scripts/fetch.sh +8 -7
  33. package/skills/read/scripts/fetch_feishu.py +11 -6
  34. package/skills/think/SKILL.md +33 -8
  35. package/skills/write/SKILL.md +88 -10
  36. package/skills/write/references/write-en.md +19 -17
  37. package/skills/write/references/write-product-localization.md +43 -0
  38. package/skills/write/references/write-zh-bilingual.md +2 -3
  39. package/skills/write/references/write-zh-prose.md +2 -0
  40. package/skills/write/references/write-zh.md +144 -68
  41. package/skills/read/references/save-paths.md +0 -33
@@ -25,10 +25,32 @@ URL_PREFIXES = ("http://", "https://", "mailto:", "ftp://", "tel:", "data:")
25
25
  SEP_RE = re.compile(r'^[\s|:\-]+$')
26
26
  PERSONAL_PATH_PATTERN = re.compile(r'/(?:Users|home)/[A-Za-z0-9._-]+/')
27
27
  SKILL_REF_RE = re.compile(r'skills/([a-z][a-z0-9_-]*)/SKILL\.md')
28
+ # Quoted user-utterance triggers in rules/waza-routing.md. Covers straight ("),
29
+ # curly (U+201C/D), and CJK corner brackets (「」 U+300C/D, 『』 U+300E/F) so a
30
+ # Chinese phrase in 「」 is checked just like one in straight quotes.
31
+ QUOTED_PHRASE_RE = re.compile(
32
+ r'"([^"]+)"'
33
+ r'|\u201c([^\u201d]+)\u201d'
34
+ r'|\u300c([^\u300d]+)\u300d'
35
+ r'|\u300e([^\u300f]+)\u300f'
36
+ )
37
+ PROJECT_RITUAL_RE = re.compile(r'\b(?:Sparkle|MAS|Homebrew tap|Xcode scheme)\b', re.IGNORECASE)
38
+ PRIVATE_CONTEXT_RE = re.compile(
39
+ r'(?:\.codex/(?:sessions|memories)|rollout_summaries/|'
40
+ r'thread_id|rollout_path|session_meta|owner/private-repo|'
41
+ r'private[-_/](?:repo|project|tool)|internal[-_/](?:repo|project|tool))',
42
+ re.IGNORECASE,
43
+ )
44
+ FORCED_GITHUB_TOOL_RE = re.compile(
45
+ r'(?:Use\s+`?gh`?\s+CLI\s+for\s+all\s+GitHub\s+interactions|'
46
+ r'for\s+all\s+GitHub\s+interactions,\s+not\s+MCP\s+or\s+raw\s+API)',
47
+ re.IGNORECASE,
48
+ )
28
49
 
29
50
  DURABLE_CONTEXT_SKILLS = {"think", "check", "hunt", "design", "write", "health"}
30
51
 
31
52
  NINJA_PREFIX = "Prefix your first line with 🥷 inline, not as its own paragraph."
53
+ OUTCOME_CONTRACT_FIELDS = ("Outcome:", "Done when:", "Evidence:", "Output:")
32
54
 
33
55
  # Attribution strings that indicate AI co-authorship leaked into tracked files.
34
56
  ATTRIBUTION_PATTERNS = (
@@ -226,6 +248,26 @@ def check_description_conformance(skill_descriptions: dict[str, str]):
226
248
  print(f"ok: description {skill} ({length} chars)")
227
249
 
228
250
 
251
+ def check_outcome_contract(skill_files: list[Path]):
252
+ """Keep skill entrypoints outcome-first instead of process-heavy."""
253
+ for path in skill_files:
254
+ text = path.read_text()
255
+ if "## Outcome Contract" not in text:
256
+ fail(
257
+ f"MISSING OUTCOME CONTRACT: {path}\n"
258
+ f" Skill entrypoints must name outcome, done state, evidence, and output before detailed workflow."
259
+ )
260
+ section = text.split("## Outcome Contract", 1)[1]
261
+ section = section.split("\n## ", 1)[0]
262
+ missing = [field for field in OUTCOME_CONTRACT_FIELDS if field not in section]
263
+ if missing:
264
+ fail(
265
+ f"INCOMPLETE OUTCOME CONTRACT: {path}\n"
266
+ f" Missing fields: {', '.join(missing)}"
267
+ )
268
+ print(f"ok: outcome contract {path.parent.name}")
269
+
270
+
229
271
  def check_durable_context_and_paths(root: Path, skill_files: list[Path]):
230
272
  """Durable context rules must stay portable and evidence-bound.
231
273
 
@@ -283,6 +325,53 @@ def check_durable_context_and_paths(root: Path, skill_files: list[Path]):
283
325
  print(f"ok: durable context preflight for {skill}")
284
326
 
285
327
 
328
+ def check_portable_skill_surface(root: Path, markdown_paths: list[Path]):
329
+ """Guard Waza's public skill surface against private/project-specific drift.
330
+
331
+ Waza skills should teach transferable workflow behavior. Project-specific
332
+ release rituals and platform products are warning signals, while
333
+ one-machine paths and platform-forcing commands are hard portability failures.
334
+ """
335
+ scan_paths = list(markdown_paths)
336
+ scan_paths.extend(sorted((root / "rules").glob("*.md")))
337
+ agents = root / "AGENTS.md"
338
+ if agents.exists():
339
+ scan_paths.append(agents)
340
+
341
+ seen: set[Path] = set()
342
+ warning_paths: list[str] = []
343
+ for path in scan_paths:
344
+ if path in seen or not path.exists():
345
+ continue
346
+ seen.add(path)
347
+ rel = path.relative_to(root)
348
+ text = path.read_text()
349
+ if "~/Downloads" in text:
350
+ fail(
351
+ f"NON-PORTABLE DEFAULT SAVE PATH: {rel}\n"
352
+ f" Use a user-specified directory, project scratch path, or session temp directory."
353
+ )
354
+ if FORCED_GITHUB_TOOL_RE.search(text):
355
+ fail(
356
+ f"FORCED GITHUB TOOLING IN GENERIC SURFACE: {rel}\n"
357
+ f" GitHub projects may prefer gh, but Waza must derive platform tools from project context."
358
+ )
359
+ if PRIVATE_CONTEXT_RE.search(text):
360
+ fail(
361
+ f"PRIVATE PROJECT OR SESSION CONTEXT IN PORTABLE SURFACE: {rel}\n"
362
+ f" Public skills and rules must not copy private project names, session paths, "
363
+ f"memory paths, rollout metadata, support vendors, or thread identifiers."
364
+ )
365
+ if PROJECT_RITUAL_RE.search(text):
366
+ warning_paths.append(rel.as_posix())
367
+ if warning_paths:
368
+ print(
369
+ "warn: project-specific names or platform products in portable surface "
370
+ f"({', '.join(warning_paths[:8])})"
371
+ )
372
+ print("ok: portable skill surface")
373
+
374
+
286
375
  def check_resolver(root: Path, skill_names: set[str]):
287
376
  """Every skill must be referenced from skills/RESOLVER.md.
288
377
 
@@ -390,7 +479,13 @@ def check_no_root_skill(root: Path):
390
479
  def check_rules_files_present(root: Path):
391
480
  """Required shared rule files outside skills/ that the per-skill ref check
392
481
  doesn't cover."""
393
- required = ["english.md", "chinese.md", "anti-patterns.md", "durable-context.md"]
482
+ required = [
483
+ "english.md",
484
+ "chinese.md",
485
+ "anti-patterns.md",
486
+ "durable-context.md",
487
+ "waza-routing.md",
488
+ ]
394
489
  for name in required:
395
490
  path = root / "rules" / name
396
491
  if not path.exists():
@@ -398,6 +493,139 @@ def check_rules_files_present(root: Path):
398
493
  print(f"ok: rules/ files present ({', '.join(required)})")
399
494
 
400
495
 
496
+ def check_anti_patterns_contract(root: Path):
497
+ """Keep shared anti-pattern rules generic and mechanically sane."""
498
+ path = root / "rules" / "anti-patterns.md"
499
+ if not path.exists():
500
+ fail(f"MISSING ANTI-PATTERNS: expected {path}")
501
+
502
+ text = path.read_text()
503
+ if re.search(r"\bWaza\b", text):
504
+ fail(
505
+ "ANTI-PATTERN PROJECT NAME LEAK: rules/anti-patterns.md\n"
506
+ " Anti-pattern rules are shared behavior. Keep row wording generic, "
507
+ "without repo or product names."
508
+ )
509
+
510
+ stale_terms = (
511
+ "Private rule leak",
512
+ "Project fact promoted to global skill",
513
+ "public Waza rules",
514
+ "reusable Waza skill",
515
+ "Keep Waza generic",
516
+ )
517
+ for term in stale_terms:
518
+ if term in text:
519
+ fail(
520
+ f"ANTI-PATTERN STALE SPECIALIZATION: {term!r}\n"
521
+ " Merge private-context and project-fact cases into one generic public-surface rule."
522
+ )
523
+
524
+ rows: list[tuple[int, str]] = []
525
+ for line in text.splitlines():
526
+ if not line.startswith("| ") or line.startswith("|---"):
527
+ continue
528
+ cells = [cell.strip() for cell in line.strip("|").split("|")]
529
+ if not cells or not cells[0].isdigit():
530
+ continue
531
+ rows.append((int(cells[0]), cells[1] if len(cells) > 1 else ""))
532
+
533
+ expected_numbers = list(range(1, len(rows) + 1))
534
+ actual_numbers = [number for number, _pattern in rows]
535
+ if actual_numbers != expected_numbers:
536
+ fail(
537
+ "ANTI-PATTERN NUMBERING DRIFT: rules/anti-patterns.md\n"
538
+ f" Expected contiguous numbering {expected_numbers}; got {actual_numbers}."
539
+ )
540
+
541
+ pattern_names: dict[str, int] = {}
542
+ for number, pattern in rows:
543
+ key = pattern.lower()
544
+ if key in pattern_names:
545
+ fail(
546
+ "ANTI-PATTERN DUPLICATE NAME: rules/anti-patterns.md\n"
547
+ f" Rows {pattern_names[key]} and {number} both use {pattern!r}."
548
+ )
549
+ pattern_names[key] = number
550
+
551
+ print("ok: anti-patterns contract")
552
+
553
+
554
+ def check_waza_routing_skills(root: Path, skill_names: set[str]):
555
+ """rules/waza-routing.md routing table must enumerate exactly the skills
556
+ under skills/. Structural drift only -- trigger phrases stay hand-tuned."""
557
+ path = root / "rules" / "waza-routing.md"
558
+ if not path.exists():
559
+ return
560
+ listed: set[str] = set()
561
+ for line in path.read_text().splitlines():
562
+ if not line.startswith("|"):
563
+ continue
564
+ cells = [c.strip() for c in line.split("|")]
565
+ if len(cells) < 3:
566
+ continue
567
+ name = cells[1]
568
+ # Skip table header (literal "skill") and separator rows ("---").
569
+ if name == "skill" or set(name) <= {"-", ":"}:
570
+ continue
571
+ if re.fullmatch(r"[a-z][a-z0-9_-]*", name):
572
+ listed.add(name)
573
+ missing = skill_names - listed
574
+ extra = listed - skill_names
575
+ if missing:
576
+ fail(
577
+ "WAZA ROUTING MISSING SKILLS: rules/waza-routing.md table omits: "
578
+ f"{', '.join(sorted(missing))}"
579
+ )
580
+ if extra:
581
+ fail(
582
+ "WAZA ROUTING STALE SKILLS: rules/waza-routing.md lists skills "
583
+ f"not in skills/: {', '.join(sorted(extra))}"
584
+ )
585
+ print(f"ok: rules/waza-routing.md skills match ({len(listed)} skills)")
586
+
587
+
588
+ def check_waza_routing_triggers(root: Path):
589
+ """Quoted user-utterance triggers in rules/waza-routing.md must be grounded.
590
+
591
+ The routing table's prose stays hand-tuned, but any phrase in quotes is a
592
+ claim about what a user literally types. Each quoted phrase (split on '/',
593
+ whitespace-normalized) must appear in the matching skill's when_to_use, so
594
+ the routing hint can never advertise a trigger no skill actually claims.
595
+ Unquoted wording is intentionally free and is not checked here.
596
+ """
597
+ path = root / "rules" / "waza-routing.md"
598
+ if not path.exists():
599
+ return
600
+ norm = lambda s: re.sub(r"\s+", "", s) # noqa: E731
601
+ for line in path.read_text().splitlines():
602
+ if not line.startswith("|"):
603
+ continue
604
+ cells = [c.strip() for c in line.split("|")]
605
+ if len(cells) < 3:
606
+ continue
607
+ skill = cells[1]
608
+ if not re.fullmatch(r"[a-z][a-z0-9_-]*", skill):
609
+ continue
610
+ skill_md = root / "skills" / skill / "SKILL.md"
611
+ if not skill_md.exists():
612
+ continue # missing skill dir is check_waza_routing_skills' job
613
+ when = norm(parse_frontmatter(skill_md)["when_to_use"])
614
+ for match in QUOTED_PHRASE_RE.finditer(cells[2]):
615
+ quoted = next(g for g in match.groups() if g is not None)
616
+ for seg in quoted.split("/"):
617
+ seg_norm = norm(seg)
618
+ if seg_norm and seg_norm not in when:
619
+ fail(
620
+ f"WAZA ROUTING UNGROUNDED TRIGGER: rules/waza-routing.md "
621
+ f"row '{skill}' quotes {seg!r}, but it is absent from "
622
+ f"skills/{skill}/SKILL.md when_to_use.\n"
623
+ f" Quote only phrases a user actually types; align the "
624
+ f"phrase with when_to_use or add it to when_to_use."
625
+ )
626
+ print("ok: rules/waza-routing.md quoted triggers grounded")
627
+
628
+
401
629
  def check_readme_install_command(root: Path):
402
630
  """README must show the default install command users can copy-paste."""
403
631
  readme = root / "README.md"
@@ -410,7 +638,67 @@ def check_readme_install_command(root: Path):
410
638
  f"README INSTALL COMMAND: README.md must include {expected!r}\n"
411
639
  f" Waza's public install path depends on this exact string."
412
640
  )
413
- print("ok: README installs nested skills")
641
+ expected_pi = "pi install npm:@tw93/waza"
642
+ if expected_pi not in text:
643
+ fail(
644
+ f"README PI INSTALL COMMAND: README.md must include {expected_pi!r}\n"
645
+ f" The Pi package install path depends on this exact string."
646
+ )
647
+ expected_agents = {
648
+ "Antigravity": "npx skills add tw93/Waza -a antigravity -g -y",
649
+ "Antigravity CLI": "npx skills add tw93/Waza -a antigravity-cli -g -y",
650
+ "OpenCode": "npx skills add tw93/Waza -a opencode -g -y",
651
+ }
652
+ for label, command in expected_agents.items():
653
+ if command not in text:
654
+ fail(
655
+ f"README {label.upper()} INSTALL COMMAND: README.md must "
656
+ f"include {command!r}\n"
657
+ f" Waza's documented agent support depends on this exact string."
658
+ )
659
+ expected_installers = {
660
+ "setup-rule": "https://github.com/tw93/Waza/releases/latest/download/setup-rule.sh",
661
+ "setup-statusline": "https://github.com/tw93/Waza/releases/latest/download/setup-statusline.sh",
662
+ }
663
+ for label, url in expected_installers.items():
664
+ if url not in text:
665
+ fail(
666
+ f"README {label.upper()} URL: README.md must include {url!r}\n"
667
+ f" Installer snippets should follow the latest release asset "
668
+ f"without per-release README churn."
669
+ )
670
+ print(
671
+ "ok: README installs nested skills, Pi package, Antigravity, OpenCode, "
672
+ "and latest installer assets"
673
+ )
674
+
675
+
676
+ def check_release_workflow_npm_surface(root: Path):
677
+ """GitHub releases must publish the npm package that Pi consumes."""
678
+ workflow = root / ".github" / "workflows" / "release.yml"
679
+ if not workflow.exists():
680
+ fail(f"MISSING RELEASE WORKFLOW: expected {workflow}")
681
+ text = workflow.read_text()
682
+ required = {
683
+ "npm publish": "publishes @tw93/waza during release",
684
+ "npm view @tw93/waza": "re-reads the npm registry after publish",
685
+ "id-token: write": "allows npm trusted publishing through GitHub OIDC",
686
+ "node-version: 24": "uses a Node/npm runtime that supports trusted publishing",
687
+ "package-manager-cache: false": "keeps release publish jobs from caching credentials or package state",
688
+ "github.event.release.tag_name": "checks the GitHub release tag",
689
+ "package.json').pi.skills[0]": "checks Pi package metadata",
690
+ "dist-tags.latest": "confirms the npm latest dist-tag",
691
+ "scripts/setup-rule.sh": "uploads the rule installer as a latest release asset",
692
+ "scripts/setup-statusline.sh": "uploads the statusline installer as a latest release asset",
693
+ }
694
+ missing = [label for label, reason in required.items() if label not in text]
695
+ if missing:
696
+ fail(
697
+ "RELEASE WORKFLOW NPM SURFACE: .github/workflows/release.yml "
698
+ "must publish and verify @tw93/waza for Pi installs.\n"
699
+ + "\n".join(f" missing {label!r}: {required[label]}" for label in missing)
700
+ )
701
+ print("ok: release workflow publishes npm package and installer assets")
414
702
 
415
703
 
416
704
  def check_english_coaching_guard(root: Path):
@@ -88,8 +88,8 @@ read_highwater() {
88
88
 
89
89
  # apply_hw: compares live vs high-water marks for a single counter (5h or 7d).
90
90
  # Mutates four caller-scope globals by name (no return, by design):
91
- # applied_pct, applied_reset values to render in the statusline now
92
- # applied_hw_pct, applied_hw_reset values to persist back to highwater.json
91
+ # applied_pct, applied_reset : values to render in the statusline now
92
+ # applied_hw_pct, applied_hw_reset : values to persist back to highwater.json
93
93
  # Caller must read these immediately after the call; the next invocation
94
94
  # clobbers them. Side effect is intentional: bash can't return composite values
95
95
  # cleanly, and threading four out-params through every call site was worse.
@@ -132,18 +132,10 @@ apply_hw() {
132
132
  applied_hw_reset="$hw_reset"
133
133
  return
134
134
  fi
135
- if [ "$hw_ok" = "0" ] || [ "$live_pct" -gt "$hw_pct" ] 2>/dev/null; then
136
- applied_pct="$live_pct"
137
- applied_reset="$live_reset"
138
- applied_hw_pct="$live_pct"
139
- applied_hw_reset="$live_reset"
140
- return
141
- fi
142
-
143
- applied_pct="$hw_pct"
144
- applied_reset="${live_reset:-$hw_reset}"
145
- applied_hw_pct="$hw_pct"
146
- applied_hw_reset="$hw_reset"
135
+ applied_pct="$live_pct"
136
+ applied_reset="$live_reset"
137
+ applied_hw_pct="$live_pct"
138
+ applied_hw_reset="$live_reset"
147
139
  }
148
140
 
149
141
  write_highwater() {
@@ -49,7 +49,7 @@ def main() -> int:
49
49
 
50
50
  # The packager rewrites `skills/<name>/SKILL.md` references to the inlined
51
51
  # section name. Any stragglers indicate a regex bug in the rewriter.
52
- for skill in ("check", "think"):
52
+ for skill in EXPECTED_SKILLS:
53
53
  if f"skills/{skill}/SKILL.md" in text:
54
54
  print(
55
55
  "POST-PACKAGE ERROR: root SKILL.md still contains nested "
@@ -23,6 +23,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
23
23
 
24
24
  from skill_frontmatter import fail # noqa: E402
25
25
  from skill_checks import ( # noqa: E402
26
+ check_anti_patterns_contract,
26
27
  check_attribution_leak,
27
28
  check_description_conformance,
28
29
  check_durable_context_and_paths,
@@ -30,13 +31,18 @@ from skill_checks import ( # noqa: E402
30
31
  check_marketplace,
31
32
  check_markdown_links,
32
33
  check_no_root_skill,
34
+ check_outcome_contract,
35
+ check_portable_skill_surface,
33
36
  check_readme_install_command,
37
+ check_release_workflow_npm_surface,
34
38
  check_references,
35
39
  check_resolver,
36
40
  check_rules_files_present,
37
41
  check_skill_files,
38
42
  check_table_pipes,
39
43
  check_trigger_overlap,
44
+ check_waza_routing_skills,
45
+ check_waza_routing_triggers,
40
46
  collect_all_md,
41
47
  )
42
48
 
@@ -65,6 +71,7 @@ def main() -> int:
65
71
  skill_files, skill_descriptions, skill_keywords = check_skill_files(root)
66
72
  skill_names = set(skill_descriptions)
67
73
  check_description_conformance(skill_descriptions)
74
+ check_outcome_contract(skill_files)
68
75
  if (root / "rules" / "durable-context.md").exists():
69
76
  check_durable_context_and_paths(root, skill_files)
70
77
 
@@ -85,12 +92,17 @@ def main() -> int:
85
92
  check_references(root, skill_files)
86
93
  resolver_path = check_resolver(root, skill_names)
87
94
  all_md = collect_all_md(root, skill_names, resolver_path)
95
+ check_portable_skill_surface(root, all_md)
88
96
  check_markdown_links(root, all_md)
89
97
  check_table_pipes(root, all_md)
90
98
  check_no_root_skill(root)
91
99
  check_trigger_overlap(skill_keywords)
92
100
  check_rules_files_present(root)
101
+ check_anti_patterns_contract(root)
102
+ check_waza_routing_skills(root, skill_names)
103
+ check_waza_routing_triggers(root)
93
104
  check_readme_install_command(root)
105
+ check_release_workflow_npm_surface(root)
94
106
  check_english_coaching_guard(root)
95
107
  check_attribution_leak(root)
96
108
  return 0
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Shared Output Marker
4
4
 
5
- 所有技能都沿用同一个输出约定:首行内联带上 `🥷`,不要单独起段。这个约定写在各自的 `SKILL.md` 里,`verify-skills.sh` 也会校验它。
5
+ 所有技能都沿用同一个输出约定:首行内联带上 `🥷`,不要单独起段。这个约定写在各自的 `SKILL.md` 里,`scripts/verify_skills.py` 也会校验它。
6
6
 
7
- 触发词到技能的路由表。Claude Code 通过每个 SKILL.md 的 `description` 自动匹配,这份文档是给人看的集中索引,也是 `verify-skills.sh` 的校验依据。改 SKILL.md 的适用范围时,同步改这里。
7
+ 触发词到技能的路由表。Claude Code 通过每个 SKILL.md 的 `description` 自动匹配,这份文档是给人看的集中索引,也是 `scripts/verify_skills.py` 的校验依据。改 SKILL.md 的适用范围时,同步改这里。
8
8
 
9
9
  > **Read the skill file before acting.** 两个技能都可能匹配时,两个都读。它们设计成可串联(例:`/think` → 实现 → `/check`)。
10
10
 
@@ -38,7 +38,7 @@
38
38
  | 触发 | 技能 |
39
39
  |------|------|
40
40
  | 消息含 http(s) URL / 任何网页链接 / PDF 路径 / "看一下这个", "读一下这个" | `skills/read/SKILL.md` |
41
- | 写作 / 改稿 / 润色 / 去 AI 味(中英文) / 推特推文 / 社交媒体文案 / launch copy / release notes 文案 | `skills/write/SKILL.md` |
41
+ | 写作 / 改稿 / 润色 / 去 AI 味(中英文) / 本地化文案 / 多语言产品文案 / 推特推文 / 社交媒体文案 / launch copy / release notes 文案 | `skills/write/SKILL.md` |
42
42
  | 文档审阅 / 白皮书 / release notes prose 审核 / "审稿" / "check this document" | `skills/write/SKILL.md` (Document Review Mode) |
43
43
  | 深度研究一个陌生领域 / 六阶段研究到成稿 / 一批材料沉淀成文章 | `skills/learn/SKILL.md` |
44
44
 
@@ -59,19 +59,19 @@
59
59
 
60
60
  ## Chaining(常见串联)
61
61
 
62
- 技能之间的转换需要用户手动触发,不会自动串联。每个技能完成后会停下来,等你决定下一步。
62
+ 技能之间默认不自动串联。每个技能完成后会停下来,等用户决定下一步,除非当前请求或项目公开上下文已经明确授权后续动作(例如 "implement this plan", "review then ship if green", "triage and close")。
63
63
 
64
64
  - `/think` 出方案 → **用户说"实现"** → 实施 → **用户说"/check"** → `/check` 把关
65
65
  - `/think` 出可执行计划 → **用户说"Implement the plan / 可以干 / 直接改"** → 按计划实施,不重新争论方向
66
66
  - `/hunt` 修复 issue → **用户说"发布 / push / 关闭 issue"** → `/check` 做发布前检查和收尾
67
- - `/read` 取回多篇 URL → **用户说"/learn"** → `/learn` 综合成文
67
+ - `/read` 取回多篇 URL → **用户说"/learn"** → `/learn` 综合成文;如果同一回合已经明确要求总结或分析,`/read` fetch 后直接满足该请求
68
68
  - `/learn` 出初稿 → **用户说"/write"** → `/write` 去 AI 味
69
69
  - `/hunt` 定位根因 → **用户说"修"** → 修完 → **用户说"/check"** → `/check` 确认没副作用
70
70
  - `/health` 发现 skill 配置问题 → **用户说"修"** → 修完 → **用户说"/health"** → 再跑一次 `/health`
71
71
 
72
72
  ## Latent vs Deterministic
73
73
 
74
- Waza 的技能都是 fat skill(Markdown 判断),底层的确定性约束走 `scripts/verify-skills.sh` 和 `rules/*.md`。新加能力时先问:
74
+ Waza 的技能都是 fat skill(Markdown 判断),底层的确定性约束走 `scripts/verify_skills.py` 和 `rules/*.md`。新加能力时先问:
75
75
 
76
76
  - 需要判断 / 适应场景 / 追问用户?→ skill
77
77
  - 同入同出 / 只是校验和列举?→ script 或 rule
@@ -83,8 +83,8 @@ Waza 的技能都是 fat skill(Markdown 判断),底层的确定性约束
83
83
  通用程序员能力沉淀在 Waza。遇到具体项目时,先从公开项目上下文提炼约束,再执行对应技能:
84
84
 
85
85
  - `code-review` / `/check` -> 从 diff、README、manifest、CI、release notes 中提炼验证命令、生成物、风险、safety sinks 和发布规则。
86
- - `github-ops` -> 复用 `skills/check/SKILL.md` Triage Mode,并从 issue/PR 现场确认 repo、发布状态和回复语言。
87
- - `release` -> 从项目公开发布文档、脚本和 CI 中确认前置条件、产物和验证命令。
86
+ - GitHub issue/PR/release intents -> `skills/check/SKILL.md` 处理。目标是 GitHub 时优先使用 `gh` 或可用的 GitHub 工具;非 GitHub 平台按项目公开上下文选择对应 CLI/API。
87
+ - Release/publish intents -> 由 `skills/check/SKILL.md` 从项目公开发布文档、脚本和 CI 中确认前置条件、产物和验证命令。
88
88
 
89
89
  本地 durable memory / preview 可以作为可选私有上下文来理解用户偏好、旧决策和可迁移模式;它不属于公开项目约束,且必须用当前代码、日志、测试、文档或远端状态重新验证。
90
90