trae-coding-engine 0.1.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 (57) hide show
  1. package/README.md +91 -0
  2. package/bin/coding-engine.js +16 -0
  3. package/lib/cli.js +86 -0
  4. package/lib/index.js +6 -0
  5. package/lib/setup.js +290 -0
  6. package/package.json +32 -0
  7. package/templates/.agents/skills/coding-architecture/SKILL.md +347 -0
  8. package/templates/.agents/skills/coding-code-review/SKILL.md +95 -0
  9. package/templates/.agents/skills/coding-cve-remediation/SKILL.md +163 -0
  10. package/templates/.agents/skills/coding-db-schema/SKILL.md +273 -0
  11. package/templates/.agents/skills/coding-deploy-config/SKILL.md +129 -0
  12. package/templates/.agents/skills/coding-docs-audit/SKILL.md +285 -0
  13. package/templates/.agents/skills/coding-docs-audit-autofix/SKILL.md +33 -0
  14. package/templates/.agents/skills/coding-http-api/SKILL.md +269 -0
  15. package/templates/.agents/skills/coding-issue-autodev/SKILL.md +172 -0
  16. package/templates/.agents/skills/coding-merge-request/SKILL.md +199 -0
  17. package/templates/.agents/skills/coding-observability/SKILL.md +193 -0
  18. package/templates/.agents/skills/coding-refactor-insight/SKILL.md +89 -0
  19. package/templates/.agents/skills/coding-release-merge/SKILL.md +224 -0
  20. package/templates/.agents/skills/coding-runtime-adapter/SKILL.md +115 -0
  21. package/templates/.agents/skills/coding-testing/SKILL.md +169 -0
  22. package/templates/AGENTS.md +179 -0
  23. package/templates/MR-Template-Default.md +33 -0
  24. package/templates/Makefile +370 -0
  25. package/templates/REGISTRY.md +53 -0
  26. package/templates/scripts/check-adr.sh +283 -0
  27. package/templates/scripts/check-comment-i18n.py +209 -0
  28. package/templates/scripts/check-comment-i18n.sh +38 -0
  29. package/templates/scripts/check-doc-refs.sh +40 -0
  30. package/templates/scripts/check-docs.sh +103 -0
  31. package/templates/scripts/check-go-names.sh +59 -0
  32. package/templates/scripts/check-iam-actions.py +126 -0
  33. package/templates/scripts/check-migration-sql-immutability.sh +232 -0
  34. package/templates/scripts/check-mr-desc.sh +165 -0
  35. package/templates/scripts/check-mr-size.sh +128 -0
  36. package/templates/scripts/check-mr-title.sh +123 -0
  37. package/templates/scripts/check-openapi.py +221 -0
  38. package/templates/scripts/check-rdb-structured-queries.sh +98 -0
  39. package/templates/scripts/check-repository-transactions.sh +100 -0
  40. package/templates/scripts/check-runbooks.sh +229 -0
  41. package/templates/scripts/check-skill-router-sync.py +331 -0
  42. package/templates/scripts/check-skills.sh +243 -0
  43. package/templates/scripts/docs-audit-to-issues.py +373 -0
  44. package/templates/scripts/docs-verification-reminder.sh +63 -0
  45. package/templates/scripts/find-ci-failures.sh +133 -0
  46. package/templates/scripts/find-stale-mrs.sh +72 -0
  47. package/templates/scripts/gen-adr-index.sh +122 -0
  48. package/templates/scripts/open-mr.sh +536 -0
  49. package/templates/scripts/regen-agent-md.sh +149 -0
  50. package/templates/scripts/run-mr-hygiene.sh +140 -0
  51. package/templates/scripts/runbook.sh +91 -0
  52. package/templates/scripts/self-maintenance-digest.py +125 -0
  53. package/templates/scripts/send-self-maintenance-lark-card.py +116 -0
  54. package/templates/scripts/skill-graph.sh +114 -0
  55. package/templates/scripts/skill-impact.sh +134 -0
  56. package/templates/scripts/skill-propose.sh +302 -0
  57. package/templates/scripts/skill-router-hook.sh +152 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ # Run all MR hygiene checks and print a compact failure summary at the end.
3
+
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")/.."
6
+
7
+ # shellcheck source=scripts/lib/cli-args.sh
8
+ . scripts/lib/cli-args.sh
9
+
10
+ DESC_FILE=""
11
+ TARGET_BRANCH="${CI_MERGE_REQUEST_TARGET_BRANCH:-release}"
12
+ FRESHNESS_TARGET=""
13
+
14
+ while [ $# -gt 0 ]; do
15
+ case "$1" in
16
+ --description-file)
17
+ DESC_FILE=$(require_value --description-file "${2:-}"); shift 2 ;;
18
+ --description-file=*)
19
+ DESC_FILE="${1#--description-file=}"; shift ;;
20
+ --target-branch)
21
+ TARGET_BRANCH=$(require_value --target-branch "${2:-}"); shift 2 ;;
22
+ --target-branch=*)
23
+ TARGET_BRANCH="${1#--target-branch=}"; shift ;;
24
+ --freshness-target)
25
+ FRESHNESS_TARGET=$(require_value --freshness-target "${2:-}"); shift 2 ;;
26
+ --freshness-target=*)
27
+ FRESHNESS_TARGET="${1#--freshness-target=}"; shift ;;
28
+ -h|--help)
29
+ sed -n '2,28p' "$0"; exit 0 ;;
30
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
31
+ esac
32
+ done
33
+
34
+ if [ -z "$FRESHNESS_TARGET" ]; then
35
+ FRESHNESS_TARGET="${MR_TARGET_REF:-origin/${TARGET_BRANCH}}"
36
+ fi
37
+
38
+ TMP_DIR=$(mktemp -d)
39
+ trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
40
+
41
+ FAILURES=()
42
+ SETUP_FAILURES=()
43
+
44
+ record_failure() {
45
+ local name=$1 rc=$2 hint=$3
46
+ if [ "$rc" -eq 2 ]; then
47
+ SETUP_FAILURES+=("${name}: setup error (exit ${rc})")
48
+ else
49
+ FAILURES+=("${name}: failed (exit ${rc})")
50
+ fi
51
+ HINTS+=("${name}: ${hint}")
52
+ }
53
+
54
+ HINTS=()
55
+
56
+ run_check() {
57
+ local name=$1 hint=$2
58
+ shift 2
59
+ local out="$TMP_DIR/${name//[^a-zA-Z0-9_]/_}.log"
60
+
61
+ printf '\n========== MR hygiene: %s ==========\n' "$name"
62
+ local rc
63
+ if "$@" >"$out" 2>&1; then
64
+ rc=0
65
+ else
66
+ rc=$?
67
+ fi
68
+ cat "$out"
69
+
70
+ if [ "$rc" -eq 0 ]; then
71
+ printf '[OK] %s\n' "$name"
72
+ return 0
73
+ fi
74
+
75
+ printf '[FAIL] %s exited with %d\n' "$name" "$rc"
76
+ record_failure "$name" "$rc" "$hint"
77
+ return 0
78
+ }
79
+
80
+ TITLE_CMD=(bash scripts/check-mr-title.sh)
81
+ DESC_CMD=(bash scripts/check-mr-desc.sh)
82
+ if [ -n "$DESC_FILE" ]; then
83
+ DESC_CMD+=(--description-file "$DESC_FILE")
84
+ fi
85
+
86
+ SIZE_BASE="origin/${TARGET_BRANCH}"
87
+ SIZE_CMD=(bash scripts/check-mr-size.sh --base "$SIZE_BASE")
88
+ FRESHNESS_CMD=(bash scripts/check-mr-target-merged.sh --target "$FRESHNESS_TARGET")
89
+
90
+ run_check "title" \
91
+ "Fix the MR title, or test locally with: make check-mr-title TITLE=\"<title>\"" \
92
+ "${TITLE_CMD[@]}"
93
+ run_check "description" \
94
+ "Fill ## 变更说明 with real prose, then test locally with: make check-mr-desc FILE=<desc.md>" \
95
+ "${DESC_CMD[@]}"
96
+ run_check "diff size" \
97
+ "Split the MR or inspect locally with: make check-mr-size BASE=${SIZE_BASE}" \
98
+ "${SIZE_CMD[@]}"
99
+ run_check "release freshness" \
100
+ "Merge latest target locally: git fetch origin ${TARGET_BRANCH} && git merge ${FRESHNESS_TARGET}" \
101
+ "${FRESHNESS_CMD[@]}"
102
+
103
+ total_failures=$((${#FAILURES[@]} + ${#SETUP_FAILURES[@]}))
104
+ if [ "$total_failures" -eq 0 ]; then
105
+ printf '\n========== MR hygiene passed ==========\n'
106
+ exit 0
107
+ fi
108
+
109
+ cat >&2 <<EOF
110
+
111
+ !!!!!!!!!! MR hygiene failed (${total_failures} check(s)) !!!!!!!!!!
112
+ EOF
113
+
114
+ if [ "${#FAILURES[@]}" -gt 0 ]; then
115
+ printf '\nBlocking check failures:\n' >&2
116
+ for failure in "${FAILURES[@]}"; do
117
+ printf ' - %s\n' "$failure" >&2
118
+ done
119
+ fi
120
+
121
+ if [ "${#SETUP_FAILURES[@]}" -gt 0 ]; then
122
+ printf '\nSetup errors:\n' >&2
123
+ for failure in "${SETUP_FAILURES[@]}"; do
124
+ printf ' - %s\n' "$failure" >&2
125
+ done
126
+ fi
127
+
128
+ printf '\nHow to fix / reproduce:\n' >&2
129
+ for hint in "${HINTS[@]}"; do
130
+ printf ' - %s\n' "$hint" >&2
131
+ done
132
+
133
+ cat >&2 <<EOF
134
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
135
+ EOF
136
+
137
+ if [ "${#SETUP_FAILURES[@]}" -gt 0 ]; then
138
+ exit 2
139
+ fi
140
+ exit 1
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # runbook.sh — oncall keyword lookup over docs/runbooks/. Read-only, grep-only.
3
+ #
4
+ # scripts/runbook.sh <keyword>
5
+ #
6
+ # Case-insensitive search across every runbook's FILENAME and BODY; prints each
7
+ # matching runbook path with a one-line context snippet. With no argument it
8
+ # prints usage and lists every runbook. When exactly one runbook matches it also
9
+ # prints a copy-paste hint to open it — but never auto-opens anything (so it is
10
+ # safe to run mid-incident without launching a pager you didn't ask for).
11
+ #
12
+ # All comments English (check-comment-i18n gate).
13
+ set -euo pipefail
14
+
15
+ # Force a UTF-8 locale: runbooks contain multibyte text (em-dash, CJK). Under a
16
+ # C / non-UTF-8 locale, BSD grep aborts with "illegal byte sequence".
17
+ for _loc in C.UTF-8 en_US.UTF-8; do
18
+ if locale -a 2>/dev/null | grep -qx "$_loc"; then
19
+ export LC_ALL="$_loc" LANG="$_loc"
20
+ break
21
+ fi
22
+ done
23
+
24
+ cd "$(dirname "$0")/.."
25
+ RUNBOOK_DIR="docs/runbooks"
26
+
27
+ # Runbooks only — exclude the front-door READMEs, the copy-me TEMPLATE, and the
28
+ # append-only INCIDENTS log from the searchable set.
29
+ list_runbooks() {
30
+ find "$RUNBOOK_DIR" -maxdepth 1 -name '*.md' \
31
+ ! -name 'README.md' ! -name 'README.*.md' \
32
+ ! -name 'TEMPLATE.md' ! -name 'INCIDENTS.md' \
33
+ | sort
34
+ }
35
+
36
+ usage() {
37
+ cat <<EOF
38
+ Usage: scripts/runbook.sh <keyword>
39
+
40
+ Case-insensitive search across docs/runbooks/ filenames and bodies.
41
+
42
+ Available runbooks:
43
+ EOF
44
+ while IFS= read -r f; do
45
+ # First H1 line, stripped of the leading '# ', as a human title.
46
+ title=$(grep -m1 -E '^# ' "$f" | sed -E 's/^#[[:space:]]*//')
47
+ printf " %-52s %s\n" "$f" "$title"
48
+ done < <(list_runbooks)
49
+ }
50
+
51
+ # No argument (or an empty one) -> usage + full list.
52
+ if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
53
+ usage
54
+ exit 0
55
+ fi
56
+
57
+ KEYWORD=$1
58
+
59
+ # Collect matches: a runbook matches if its filename OR any body line contains
60
+ # the keyword (case-insensitive). Keep the match list ordered and unique.
61
+ matches=()
62
+ while IFS= read -r f; do
63
+ base=$(basename "$f")
64
+ if printf '%s\n' "$base" | grep -qiF -- "$KEYWORD"; then
65
+ matches+=("$f")
66
+ continue
67
+ fi
68
+ if grep -qiF -- "$KEYWORD" "$f"; then
69
+ matches+=("$f")
70
+ fi
71
+ done < <(list_runbooks)
72
+
73
+ if [ "${#matches[@]}" -eq 0 ]; then
74
+ echo "No runbook matches '$KEYWORD'."
75
+ echo "Run 'scripts/runbook.sh' with no argument to list all runbooks."
76
+ exit 1
77
+ fi
78
+
79
+ for f in "${matches[@]}"; do
80
+ echo "$f"
81
+ # One-line body context: first matching line (filename-only matches may have
82
+ # none), trimmed and truncated so the output stays a single tidy line.
83
+ ctx=$(grep -m1 -iF -- "$KEYWORD" "$f" 2>/dev/null | sed -E 's/^[[:space:]]+//' | cut -c1-100 || true)
84
+ [ -n "$ctx" ] && echo " $ctx"
85
+ done
86
+
87
+ # Exactly one match -> offer a copy-paste open hint (do NOT auto-open).
88
+ if [ "${#matches[@]}" -eq 1 ]; then
89
+ echo
90
+ echo "Open it: ${PAGER:-less} ${matches[0]}"
91
+ fi
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Aggregate open self-maintenance issues into one finder-grouped heartbeat markdown.
3
+
4
+ Read-only: consumes the issue store (open + a bounded closed scan) and renders
5
+ markdown. No issue writes, no artifact passing.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ SEVERITY_ORDER = {"Critical": 0, "Major": 1, "Minor": 2}
15
+ LIB = "scripts/lib/codebase-json.py"
16
+
17
+
18
+ def parse_args() -> argparse.Namespace:
19
+ p = argparse.ArgumentParser(description="Render the self-maintenance heartbeat markdown.")
20
+ p.add_argument("--open-json", required=True, help="open issue list JSON file")
21
+ p.add_argument("--closed-json", required=True, help="closed issue list JSON file (recurrence)")
22
+ p.add_argument("--health", default="healthy", choices=("healthy", "unhealthy", "unknown"))
23
+ return p.parse_args()
24
+
25
+
26
+ def run_codebase_json(subcommand: str, json_path: str) -> str:
27
+ data = Path(json_path).read_bytes() if Path(json_path).exists() else b""
28
+ if not data.strip():
29
+ return ""
30
+ result = subprocess.run(
31
+ [sys.executable, LIB, subcommand],
32
+ input=data, capture_output=True, check=False,
33
+ )
34
+ if result.returncode != 0:
35
+ # Fail loud: a parser/schema failure must NOT be silently treated as an
36
+ # empty issue store (that would emit a false "no backlog" / silent card).
37
+ sys.stderr.write(
38
+ f"error: {LIB} {subcommand} failed (exit {result.returncode}): "
39
+ f"{result.stderr.decode('utf-8', errors='replace')}\n"
40
+ )
41
+ raise SystemExit(result.returncode)
42
+ return result.stdout.decode("utf-8", errors="replace")
43
+
44
+
45
+ def parse_rows(text: str) -> list[dict[str, str]]:
46
+ rows: list[dict[str, str]] = []
47
+ for line in text.splitlines():
48
+ parts = line.split("\t")
49
+ if len(parts) < 6:
50
+ continue
51
+ rows.append({
52
+ "number": parts[0], "finder": parts[1], "severity": parts[2],
53
+ "dedup": parts[3], "evidence": parts[4], "title": parts[5],
54
+ })
55
+ return rows
56
+
57
+
58
+ def parse_counts(text: str) -> dict[str, int]:
59
+ counts: dict[str, int] = {}
60
+ for line in text.splitlines():
61
+ parts = line.split("\t")
62
+ if len(parts) == 2 and parts[1].isdigit():
63
+ counts[parts[0]] = int(parts[1])
64
+ return counts
65
+
66
+
67
+ def cluster_flags(rows: list[dict[str, str]]) -> dict[str, int]:
68
+ # evidence path -> number of distinct finders that touch it
69
+ by_path: dict[str, set[str]] = {}
70
+ for r in rows:
71
+ for path in filter(None, r["evidence"].split(",")):
72
+ by_path.setdefault(path, set()).add(r["finder"])
73
+ return {p: len(f) for p, f in by_path.items() if len(f) > 1}
74
+
75
+
76
+ def build_digest_markdown(rows, recurrence, health) -> str:
77
+ health_banner = (
78
+ f"## ⚠️ 平台健康:{health}\n\n本拍跳过 deep 派发;坍缩为单卡。"
79
+ if health != "healthy"
80
+ else ""
81
+ )
82
+ if not rows:
83
+ # Fail-noisy: an abnormal platform-health verdict must still surface a
84
+ # card even when the open backlog is empty (never silently skip).
85
+ if health_banner:
86
+ return health_banner + "\n\n---\n\n(本拍无 open self-maintenance issue。)"
87
+ return "(no open self-maintenance issues)"
88
+
89
+ clusters = cluster_flags(rows)
90
+ sections: list[str] = [health_banner] if health_banner else []
91
+
92
+ groups: dict[str, list[dict[str, str]]] = {}
93
+ for r in rows:
94
+ groups.setdefault(r["finder"], []).append(r)
95
+
96
+ for finder in sorted(groups):
97
+ # Documented two-level ordering: severity first, then recurrence count
98
+ # descending (a repeatedly-recurring item outranks a first-seen one of
99
+ # the same severity).
100
+ items = sorted(
101
+ groups[finder],
102
+ key=lambda r: (SEVERITY_ORDER.get(r["severity"], 2), -recurrence.get(r["dedup"], 0)),
103
+ )
104
+ lines = [f"## {finder} ({len(items)})", ""]
105
+ for r in items:
106
+ rec = recurrence.get(r["dedup"], 0)
107
+ rec_str = f" · 复发×{rec}" if rec >= 2 else ""
108
+ cluster_hit = [p for p in r["evidence"].split(",") if p in clusters]
109
+ cl_str = f" · 🔗{cluster_hit[0]}" if cluster_hit else ""
110
+ lines.append(f"- `[{r['severity']}]` #{r['number']} {r['title']}{rec_str}{cl_str}")
111
+ sections.append("\n".join(lines))
112
+
113
+ return "\n\n---\n\n".join(sections)
114
+
115
+
116
+ def main() -> int:
117
+ args = parse_args()
118
+ rows = parse_rows(run_codebase_json("issue-digest-rows", args.open_json))
119
+ recurrence = parse_counts(run_codebase_json("issue-dedup-counts", args.closed_json))
120
+ print(build_digest_markdown(rows, recurrence, args.health))
121
+ return 0
122
+
123
+
124
+ if __name__ == "__main__":
125
+ raise SystemExit(main())
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Send the single self-maintenance heartbeat card to a Lark custom bot."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import base64
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import os
11
+ import sys
12
+ import time
13
+ import urllib.error
14
+ import urllib.request
15
+ from pathlib import Path
16
+
17
+ MAX_MARKDOWN_CHARS = 14000
18
+
19
+
20
+ def parse_args() -> argparse.Namespace:
21
+ p = argparse.ArgumentParser(description="Send the self-maintenance heartbeat card.")
22
+ p.add_argument("--markdown-file", help="digest markdown file; default reads stdin")
23
+ p.add_argument("--title", default=os.getenv("HEARTBEAT_CARD_TITLE", "Coding 自维护心跳"))
24
+ p.add_argument("--webhook", default=os.getenv("LARK_CUSTOM_BOT_WEBHOOK"))
25
+ p.add_argument("--secret", default=os.getenv("LARK_CUSTOM_BOT_SECRET"))
26
+ p.add_argument("--dry-run", action="store_true")
27
+ return p.parse_args()
28
+
29
+
30
+ def build_payload(*, title: str, markdown: str, secret: str | None) -> dict[str, object]:
31
+ unhealthy = "平台健康" in markdown
32
+ # Never silently drop content: if the body exceeds the card limit, truncate
33
+ # but append a visible notice pointing at the durable issue list.
34
+ if len(markdown) > MAX_MARKDOWN_CHARS:
35
+ notice = "\n\n…(内容过长已截断,完整 backlog 见 issue 列表)"
36
+ markdown = markdown[: MAX_MARKDOWN_CHARS - len(notice)] + notice
37
+ payload: dict[str, object] = {
38
+ "msg_type": "interactive",
39
+ "card": {
40
+ "schema": "2.0",
41
+ "config": {"update_multi": True},
42
+ "header": {
43
+ "template": "red" if unhealthy else "blue",
44
+ "title": {"tag": "plain_text", "content": title},
45
+ },
46
+ "body": {
47
+ "direction": "vertical",
48
+ "padding": "12px 12px 12px 12px",
49
+ "elements": [{"tag": "markdown", "content": markdown[:MAX_MARKDOWN_CHARS]}],
50
+ },
51
+ },
52
+ }
53
+ if secret:
54
+ timestamp = str(int(time.time()))
55
+ payload["timestamp"] = timestamp
56
+ payload["sign"] = sign(timestamp, secret)
57
+ return payload
58
+
59
+
60
+ def sign(timestamp: str, secret: str) -> str:
61
+ string_to_sign = f"{timestamp}\n{secret}".encode("utf-8")
62
+ digest = hmac.new(string_to_sign, b"", hashlib.sha256).digest()
63
+ return base64.b64encode(digest).decode("utf-8")
64
+
65
+
66
+ def send(webhook: str, payload: dict[str, object]) -> int:
67
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
68
+ request = urllib.request.Request(
69
+ webhook, data=data,
70
+ headers={"Content-Type": "application/json; charset=utf-8"}, method="POST",
71
+ )
72
+ try:
73
+ with urllib.request.urlopen(request, timeout=15) as response:
74
+ body = response.read().decode("utf-8", errors="replace")
75
+ except urllib.error.HTTPError as exc:
76
+ body = exc.read().decode("utf-8", errors="replace")
77
+ print(f"error: webhook HTTP {exc.code}: {body}", file=sys.stderr)
78
+ return 1
79
+ except urllib.error.URLError as exc:
80
+ print(f"error: webhook request failed: {exc}", file=sys.stderr)
81
+ return 1
82
+ try:
83
+ decoded = json.loads(body)
84
+ except json.JSONDecodeError:
85
+ print(f"error: webhook returned non-JSON: {body}", file=sys.stderr)
86
+ return 1
87
+ code = decoded.get("code")
88
+ status_code = decoded.get("StatusCode")
89
+ if code != 0 and status_code != 0:
90
+ print(f"error: webhook rejected message: {body}", file=sys.stderr)
91
+ return 1
92
+ print("sent self-maintenance heartbeat card to Lark custom bot")
93
+ return 0
94
+
95
+
96
+ def main() -> int:
97
+ args = parse_args()
98
+ if args.markdown_file:
99
+ markdown = Path(args.markdown_file).read_text(encoding="utf-8").strip()
100
+ else:
101
+ markdown = sys.stdin.read().strip()
102
+ if not markdown or markdown == "(no open self-maintenance issues)":
103
+ print("no open self-maintenance issues; skip Lark notification")
104
+ return 0
105
+ payload = build_payload(title=args.title, markdown=markdown, secret=args.secret)
106
+ if args.dry_run:
107
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
108
+ return 0
109
+ if not args.webhook:
110
+ print("error: missing webhook. Set LARK_CUSTOM_BOT_WEBHOOK or pass --webhook.", file=sys.stderr)
111
+ return 2
112
+ return send(args.webhook, payload)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ raise SystemExit(main())
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Generates a Graphviz DOT graph of cross-references between coding-* skills.
3
+ #
4
+ # Usage:
5
+ # bash scripts/skill-graph.sh # write to docs/assets/skill-graph.dot, render PNG if dot is available
6
+ # bash scripts/skill-graph.sh --check # regenerate to a temp file, diff against committed; non-zero exit on drift
7
+ # bash scripts/skill-graph.sh --stdout # print DOT to stdout (no file write)
8
+ #
9
+ # Edges are extracted by grepping each SKILL.md for occurrences of other
10
+ # `coding-<known>` skill names (excluding self). A "known" skill is any
11
+ # directory under .agents/skills/coding-*/. Non-skill `coding-*` tokens
12
+ # (coding-server, coding-gateway, coding-worker — deployment names) are
13
+ # automatically filtered because they don't match a directory.
14
+
15
+ set -euo pipefail
16
+ shopt -s nullglob
17
+
18
+ SKILLS_ROOT=".agents/skills"
19
+ DOT_PATH="docs/assets/skill-graph.dot"
20
+ PNG_PATH="docs/assets/skill-graph.png"
21
+
22
+ mode="write"
23
+ case "${1:-}" in
24
+ --check) mode="check" ;;
25
+ --stdout) mode="stdout" ;;
26
+ "") mode="write" ;;
27
+ *) echo "usage: $0 [--check | --stdout]" >&2; exit 2 ;;
28
+ esac
29
+
30
+ # Collect known skill names from filesystem
31
+ known=()
32
+ for dir in "$SKILLS_ROOT"/coding-*; do
33
+ [[ -d "$dir" ]] || continue
34
+ known+=("$(basename "$dir")")
35
+ done
36
+
37
+ if [[ "${#known[@]}" -eq 0 ]]; then
38
+ echo "[ERROR] no skills found under $SKILLS_ROOT" >&2
39
+ exit 1
40
+ fi
41
+
42
+ # Build a regex alternation of known names for filtering
43
+ known_regex="^($(IFS='|'; echo "${known[*]}"))$"
44
+
45
+ # Emit DOT to a string we can either write or compare
46
+ emit_dot() {
47
+ cat <<EOF
48
+ // Skill cross-reference graph for coding-engine.
49
+ // Auto-generated by scripts/skill-graph.sh — do not edit by hand.
50
+ // Regenerate via: make skill-graph
51
+ //
52
+ // Render to PNG locally: dot -Tpng docs/assets/skill-graph.dot -o docs/assets/skill-graph.png
53
+ // Render to SVG locally: dot -Tsvg docs/assets/skill-graph.dot -o docs/assets/skill-graph.svg
54
+ EOF
55
+ echo ""
56
+ echo "digraph SkillCrossRefs {"
57
+ echo " rankdir=LR;"
58
+ echo " node [shape=box, style=\"rounded,filled\", fillcolor=\"#f5f5f5\", fontname=\"Helvetica\"];"
59
+ echo " edge [fontname=\"Helvetica\", color=\"#666666\"];"
60
+ echo ""
61
+
62
+ for src in "${known[@]}"; do
63
+ echo " \"$src\";"
64
+ done
65
+ echo ""
66
+
67
+ for src in "${known[@]}"; do
68
+ local skill_md="$SKILLS_ROOT/$src/SKILL.md"
69
+ [[ -f "$skill_md" ]] || continue
70
+
71
+ # Extract all coding-* tokens, dedup, filter to known set, drop self.
72
+ # Pattern is intentionally permissive (incl. digits / extra hyphens);
73
+ # known_regex below rejects anything not actually under .agents/skills/.
74
+ while IFS= read -r tgt; do
75
+ [[ -z "$tgt" ]] && continue
76
+ [[ "$tgt" == "$src" ]] && continue # skip self
77
+ [[ "$tgt" =~ $known_regex ]] || continue # filter unknown
78
+ echo " \"$src\" -> \"$tgt\";"
79
+ done < <(grep -oE 'coding-[a-z0-9][a-z0-9-]*' "$skill_md" | sort -u)
80
+ done
81
+
82
+ echo "}"
83
+ }
84
+
85
+ case "$mode" in
86
+ stdout)
87
+ emit_dot
88
+ ;;
89
+ check)
90
+ if [[ ! -f "$DOT_PATH" ]]; then
91
+ echo "[FAIL] $DOT_PATH does not exist — run \`make skill-graph\` to create it"
92
+ exit 1
93
+ fi
94
+ if ! diff -q <(emit_dot) "$DOT_PATH" >/dev/null 2>&1; then
95
+ echo "[STALE] $DOT_PATH does not match current SKILL.md cross-references"
96
+ echo " Run \`make skill-graph\` to regenerate, then commit the diff"
97
+ exit 1
98
+ fi
99
+ ;;
100
+ write)
101
+ mkdir -p "$(dirname "$DOT_PATH")"
102
+ emit_dot > "$DOT_PATH"
103
+ echo " [OK] wrote $DOT_PATH"
104
+
105
+ if command -v dot >/dev/null 2>&1; then
106
+ dot -Tpng "$DOT_PATH" -o "$PNG_PATH"
107
+ echo " [OK] rendered $PNG_PATH"
108
+ else
109
+ echo " [SKIP] graphviz \`dot\` not in PATH — PNG render skipped"
110
+ echo " install via: brew install graphviz (macOS)"
111
+ echo " apt-get install graphviz (Debian/Ubuntu)"
112
+ fi
113
+ ;;
114
+ esac