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.
- package/README.md +91 -0
- package/bin/coding-engine.js +16 -0
- package/lib/cli.js +86 -0
- package/lib/index.js +6 -0
- package/lib/setup.js +290 -0
- package/package.json +32 -0
- package/templates/.agents/skills/coding-architecture/SKILL.md +347 -0
- package/templates/.agents/skills/coding-code-review/SKILL.md +95 -0
- package/templates/.agents/skills/coding-cve-remediation/SKILL.md +163 -0
- package/templates/.agents/skills/coding-db-schema/SKILL.md +273 -0
- package/templates/.agents/skills/coding-deploy-config/SKILL.md +129 -0
- package/templates/.agents/skills/coding-docs-audit/SKILL.md +285 -0
- package/templates/.agents/skills/coding-docs-audit-autofix/SKILL.md +33 -0
- package/templates/.agents/skills/coding-http-api/SKILL.md +269 -0
- package/templates/.agents/skills/coding-issue-autodev/SKILL.md +172 -0
- package/templates/.agents/skills/coding-merge-request/SKILL.md +199 -0
- package/templates/.agents/skills/coding-observability/SKILL.md +193 -0
- package/templates/.agents/skills/coding-refactor-insight/SKILL.md +89 -0
- package/templates/.agents/skills/coding-release-merge/SKILL.md +224 -0
- package/templates/.agents/skills/coding-runtime-adapter/SKILL.md +115 -0
- package/templates/.agents/skills/coding-testing/SKILL.md +169 -0
- package/templates/AGENTS.md +179 -0
- package/templates/MR-Template-Default.md +33 -0
- package/templates/Makefile +370 -0
- package/templates/REGISTRY.md +53 -0
- package/templates/scripts/check-adr.sh +283 -0
- package/templates/scripts/check-comment-i18n.py +209 -0
- package/templates/scripts/check-comment-i18n.sh +38 -0
- package/templates/scripts/check-doc-refs.sh +40 -0
- package/templates/scripts/check-docs.sh +103 -0
- package/templates/scripts/check-go-names.sh +59 -0
- package/templates/scripts/check-iam-actions.py +126 -0
- package/templates/scripts/check-migration-sql-immutability.sh +232 -0
- package/templates/scripts/check-mr-desc.sh +165 -0
- package/templates/scripts/check-mr-size.sh +128 -0
- package/templates/scripts/check-mr-title.sh +123 -0
- package/templates/scripts/check-openapi.py +221 -0
- package/templates/scripts/check-rdb-structured-queries.sh +98 -0
- package/templates/scripts/check-repository-transactions.sh +100 -0
- package/templates/scripts/check-runbooks.sh +229 -0
- package/templates/scripts/check-skill-router-sync.py +331 -0
- package/templates/scripts/check-skills.sh +243 -0
- package/templates/scripts/docs-audit-to-issues.py +373 -0
- package/templates/scripts/docs-verification-reminder.sh +63 -0
- package/templates/scripts/find-ci-failures.sh +133 -0
- package/templates/scripts/find-stale-mrs.sh +72 -0
- package/templates/scripts/gen-adr-index.sh +122 -0
- package/templates/scripts/open-mr.sh +536 -0
- package/templates/scripts/regen-agent-md.sh +149 -0
- package/templates/scripts/run-mr-hygiene.sh +140 -0
- package/templates/scripts/runbook.sh +91 -0
- package/templates/scripts/self-maintenance-digest.py +125 -0
- package/templates/scripts/send-self-maintenance-lark-card.py +116 -0
- package/templates/scripts/skill-graph.sh +114 -0
- package/templates/scripts/skill-impact.sh +134 -0
- package/templates/scripts/skill-propose.sh +302 -0
- 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
|