@ccoalm/ccl-skills 0.6.1 → 0.7.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 +55 -15
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-unverified-cli-flag.sh +309 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_unverified_cli_flag.sh +483 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +14 -11
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +25 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +7 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +5 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/audit-history-architecture.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +7 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/notification-architecture.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/replay-comparison-architecture.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/workflow-state-architecture.md +39 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +6 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/audit-history-patterns.md +29 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +25 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/notification-patterns.md +40 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/replay-comparison-patterns.md +30 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +48 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +10 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +48 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +21 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +5 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-parallel-stack-parity.sh +119 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_parallel_stack_parity.sh +183 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +3 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +16 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +5 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +16 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/self-benchmark-baseline.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +1 -0
- package/dist/assets/release.json +115 -50
- package/package.json +3 -3
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Deterministic behavior suite for hooks/remind-unverified-cli-flag.sh.
|
|
3
|
+
# Registered in the Makefile `test` target; requires jq (the hook's dependency —
|
|
4
|
+
# without jq the hook degrades to silence, so this suite requires it and fails
|
|
5
|
+
# loudly instead of false-greening).
|
|
6
|
+
#
|
|
7
|
+
# The hook deliberately does NOT parse shell, so this suite does not probe shell
|
|
8
|
+
# forms. It probes the three things the hook actually asserts — tool-word match,
|
|
9
|
+
# long-flag presence, per-session dedup — plus the two contracts it must never
|
|
10
|
+
# break (exit 0 / valid JSON / silent stderr) and the marker-safety guards. The
|
|
11
|
+
# documented FALSE FIRES are asserted too, so they stay intentional rather than
|
|
12
|
+
# drifting into accidents.
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
16
|
+
HOOK="${CLI_FLAG_HOOK:-$SCRIPT_DIR/remind-unverified-cli-flag.sh}"
|
|
17
|
+
[ -f "$HOOK" ] || { echo "FAIL: hook not found: $HOOK" >&2; exit 1; }
|
|
18
|
+
command -v jq >/dev/null 2>&1 || { echo "FAIL: jq required for this suite" >&2; exit 1; }
|
|
19
|
+
# Fail LOUDLY on a hook syntax error rather than as a wall of mysterious probe
|
|
20
|
+
# failures — one stray apostrophe inside an embedded program did exactly that.
|
|
21
|
+
bash -n "$HOOK" 2>/dev/null || { echo "FAIL: hook is not syntactically valid" >&2; bash -n "$HOOK"; exit 1; }
|
|
22
|
+
|
|
23
|
+
pass=0; fail=0; probe_n=0
|
|
24
|
+
|
|
25
|
+
# Isolate the hook's dedup markers into a per-RUN directory. Without this the
|
|
26
|
+
# suite self-poisoned across runs: sessions derive from `$$`, the hook persists
|
|
27
|
+
# markers, and after PID reuse a stale one silently turned an expected `remind`
|
|
28
|
+
# into `quiet`.
|
|
29
|
+
MARKER_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cliflag-suite-XXXXXX") || {
|
|
30
|
+
echo "FAIL: cannot create isolated marker dir" >&2; exit 1; }
|
|
31
|
+
trap 'rm -rf "$MARKER_DIR"' EXIT INT TERM
|
|
32
|
+
export TMPDIR="$MARKER_DIR"
|
|
33
|
+
|
|
34
|
+
# The hook's core promise is that it never disturbs the Bash call: exit 0, valid
|
|
35
|
+
# JSON, silent stderr. All three were discarded by an earlier version of this
|
|
36
|
+
# suite, so a nonzero exit or a stray diagnostic passed every assertion.
|
|
37
|
+
contract_check() {
|
|
38
|
+
local out="$1" rc="$2" label="$3" errbytes
|
|
39
|
+
if [ -n "${PROBE_ERR:-}" ] && [ -f "$PROBE_ERR" ]; then
|
|
40
|
+
errbytes=$(wc -c < "$PROBE_ERR" | tr -d ' ')
|
|
41
|
+
if [ "$errbytes" != "0" ]; then
|
|
42
|
+
fail=$((fail+1)); printf 'FAIL [wrote %s bytes to stderr] %s\n' "$errbytes" "$label" >&2
|
|
43
|
+
printf ' %s\n' "$(head -1 "$PROBE_ERR")" >&2; return
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
if [ "$rc" -ne 0 ]; then
|
|
47
|
+
fail=$((fail+1)); printf 'FAIL [exit=%s, must be 0] %s\n' "$rc" "$label" >&2; return
|
|
48
|
+
fi
|
|
49
|
+
if [ -n "$out" ] && ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then
|
|
50
|
+
fail=$((fail+1)); printf 'FAIL [output is not valid JSON] %s\n' "$label" >&2; return
|
|
51
|
+
fi
|
|
52
|
+
pass=$((pass+1))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
run_hook() { # run_hook <command> <session>; sets HOOK_OUT / HOOK_RC
|
|
56
|
+
PROBE_ERR=$(mktemp "${MARKER_DIR}/err-XXXXXX")
|
|
57
|
+
HOOK_OUT=$(jq -nc --arg c "$1" --arg s "$2" \
|
|
58
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' | bash "$HOOK" 2>"$PROBE_ERR"); HOOK_RC=$?
|
|
59
|
+
contract_check "$HOOK_OUT" "$HOOK_RC" "$1"
|
|
60
|
+
rm -f "$PROBE_ERR"; PROBE_ERR=""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# probe <remind|quiet> <command> [session]
|
|
64
|
+
probe() {
|
|
65
|
+
local expect="$1" cmd="$2" sess="${3:-}" got
|
|
66
|
+
probe_n=$((probe_n+1)); [ -n "$sess" ] || sess="probe-$$-$probe_n"
|
|
67
|
+
run_hook "$cmd" "$sess"
|
|
68
|
+
got="quiet"
|
|
69
|
+
# Assert the STRUCTURE the PreToolUse consumer reads, not just the key name: a
|
|
70
|
+
# top-level `additionalContext` is valid JSON the host ignores.
|
|
71
|
+
printf '%s' "$HOOK_OUT" | jq -e \
|
|
72
|
+
'.hookSpecificOutput.hookEventName == "PreToolUse"
|
|
73
|
+
and (.hookSpecificOutput.additionalContext | type) == "string"
|
|
74
|
+
and (.hookSpecificOutput.additionalContext | length) > 0' >/dev/null 2>&1 && got="remind"
|
|
75
|
+
if [ "$got" = "$expect" ]; then pass=$((pass+1))
|
|
76
|
+
else fail=$((fail+1)); printf 'FAIL [want=%s got=%s] %s\n' "$expect" "$got" "$cmd" >&2; fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# attrib <expected tool> <command> — which tool the advisory names.
|
|
80
|
+
attrib() {
|
|
81
|
+
local want="$1" cmd="$2" got
|
|
82
|
+
probe_n=$((probe_n+1))
|
|
83
|
+
run_hook "$cmd" "attrib-$$-$probe_n"
|
|
84
|
+
got=$(printf '%s' "$HOOK_OUT" | jq -r '.hookSpecificOutput.additionalContext // ""' 2>/dev/null \
|
|
85
|
+
| grep -o '`[^`]*`' | head -1 | tr -d '`')
|
|
86
|
+
if [ "$got" = "$want" ]; then pass=$((pass+1))
|
|
87
|
+
else fail=$((fail+1)); printf 'FAIL [want-attrib=%s got=%s] %s\n' "$want" "${got:-<none>}" "$cmd" >&2; fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# --- FIRES: the recorded failure shapes this hook exists to catch ---
|
|
91
|
+
probe remind 'glab mr list --state opened'
|
|
92
|
+
probe remind 'glab ci list --branch dev'
|
|
93
|
+
probe remind 'glab pipeline view 21852 --output json'
|
|
94
|
+
probe remind 'gh pr list --state open'
|
|
95
|
+
probe remind 'lark-cli schema tbl --jq .fields'
|
|
96
|
+
probe remind 'uv publish --check-url https://example.invalid/simple'
|
|
97
|
+
attrib 'glab' 'glab mr list --state opened'
|
|
98
|
+
attrib 'uv' 'uv publish --check-url https://example.invalid/simple'
|
|
99
|
+
# No shell parsing means every spelling of the same invocation behaves alike —
|
|
100
|
+
# this is the property the fifteen-round parser was failing to achieve.
|
|
101
|
+
probe remind 'cd /some/path && glab mr list --state merged'
|
|
102
|
+
probe remind 'GITLAB_HOST=x glab mr list --state opened'
|
|
103
|
+
probe remind 'env GITLAB_HOST=x glab mr list --state opened'
|
|
104
|
+
probe remind 'command glab mr list --state opened'
|
|
105
|
+
probe remind 'gh --repo=owner/repo pr list --state open'
|
|
106
|
+
probe remind 'gh -R owner/repo pr list --state open'
|
|
107
|
+
probe remind 'glab mr list \
|
|
108
|
+
--state opened'
|
|
109
|
+
probe remind 'git --help && glab mr list --state opened'
|
|
110
|
+
|
|
111
|
+
# --- QUIET: no long flag means nothing to guess ---
|
|
112
|
+
probe quiet 'glab mr view 123'
|
|
113
|
+
probe quiet 'gh pr list'
|
|
114
|
+
probe quiet 'uv sync'
|
|
115
|
+
# --- QUIET: tools outside the list are a documented NON-fire ---
|
|
116
|
+
probe quiet 'git log --oneline --graph'
|
|
117
|
+
probe quiet 'kubectl get pods --all-namespaces'
|
|
118
|
+
probe quiet 'npm install --save-dev typescript'
|
|
119
|
+
# --- QUIET: the tool name must appear as a WORD ---
|
|
120
|
+
probe quiet 'echo glabber --state opened'
|
|
121
|
+
probe quiet './myglab --state opened'
|
|
122
|
+
# --- QUIET: whitespace-bearing quoted prose is masked ---
|
|
123
|
+
probe quiet 'git commit -m "note that glab mr list --state is unsupported"'
|
|
124
|
+
probe quiet 'git log --grep="glab mr list --state"'
|
|
125
|
+
# --- QUIET: a help request whose FIRST WORD is the tool ---
|
|
126
|
+
probe quiet 'glab mr list --help'
|
|
127
|
+
probe quiet 'glab mr list -h'
|
|
128
|
+
probe quiet 'glab help mr list --json'
|
|
129
|
+
# A bare `help` counts only in the SUBCOMMAND slot: as a positional argument it
|
|
130
|
+
# is an endpoint name, not a help request, and suppressing there swallowed the
|
|
131
|
+
# advisory for a genuine flag.
|
|
132
|
+
probe remind 'gh api help --paginate'
|
|
133
|
+
probe quiet 'gh pr merge --help'
|
|
134
|
+
# A standalone `--` hands the rest of the line to another program, so a `--help`
|
|
135
|
+
# after it is that program's, not this tool's — the flag before it still needs
|
|
136
|
+
# the advisory.
|
|
137
|
+
probe remind 'uv run --python 3.12 -- python --help'
|
|
138
|
+
# ...but suppression must NOT extend to a compound command: the first word is
|
|
139
|
+
# still the tool while the unrelated `--help` belongs to something else, and the
|
|
140
|
+
# whole-command grep swallowed the advisory for a genuine invocation.
|
|
141
|
+
probe remind 'glab mr list --state opened && git --help'
|
|
142
|
+
probe remind 'glab mr list --state opened; gh pr list --help'
|
|
143
|
+
# A NEWLINE separates commands exactly as `;` does, and grep is line-oriented so
|
|
144
|
+
# a character class can never see one — a two-line command read as "simple" and
|
|
145
|
+
# the second line's `--help` suppressed the advisory for the first line.
|
|
146
|
+
probe remind 'glab mr list --state opened
|
|
147
|
+
git --help'
|
|
148
|
+
|
|
149
|
+
# --- DOCUMENTED FALSE FIRES (asserted so they stay intentional) ---
|
|
150
|
+
# A mention inside a heredoc body fires once for that session. Accepted: chasing
|
|
151
|
+
# it is what produced the fifteen-round parser.
|
|
152
|
+
probe remind 'cat <<EOF
|
|
153
|
+
glab mr list --state opened
|
|
154
|
+
EOF'
|
|
155
|
+
# A help request that is not the command's first word still fires.
|
|
156
|
+
probe remind 'cd /x && glab mr list --help'
|
|
157
|
+
# A payload inside a whitespace-bearing quoted string is masked away and stays
|
|
158
|
+
# QUIET. The mask cannot separate a launcher payload from a commit message
|
|
159
|
+
# without parsing, which is exactly what this hook refuses to do; asserted so the
|
|
160
|
+
# trade-off stays deliberate rather than drifting into an accident.
|
|
161
|
+
probe quiet "sh -c 'glab mr list --state opened'"
|
|
162
|
+
probe quiet 'bash -c "glab mr list --state opened"'
|
|
163
|
+
|
|
164
|
+
# --- DEDUP: once per (session, tool); a different tool still fires ---
|
|
165
|
+
d="dedup-$$"
|
|
166
|
+
probe remind 'glab mr list --state opened' "$d"
|
|
167
|
+
probe quiet 'glab ci list --branch dev' "$d"
|
|
168
|
+
probe remind 'gh pr list --state open' "$d"
|
|
169
|
+
probe remind 'glab mr list --state opened' "${d}-other"
|
|
170
|
+
# A tool that always TRAILS another in the same command must still get its own
|
|
171
|
+
# advisory: taking only the first tool word meant `gh` stayed silent for the
|
|
172
|
+
# whole session once `glab` was deduped.
|
|
173
|
+
m="mask-$$"
|
|
174
|
+
probe remind 'glab mr list --state opened' "$m"
|
|
175
|
+
attrib_in_session() { # attrib_in_session <expected tool> <cmd> <session>
|
|
176
|
+
probe_n=$((probe_n+1)); run_hook "$2" "$3"
|
|
177
|
+
local got
|
|
178
|
+
got=$(printf '%s' "$HOOK_OUT" | jq -r '.hookSpecificOutput.additionalContext // ""' 2>/dev/null \
|
|
179
|
+
| grep -o '`[^`]*`' | head -1 | tr -d '`')
|
|
180
|
+
if [ "$got" = "$1" ]; then pass=$((pass+1))
|
|
181
|
+
else fail=$((fail+1)); printf 'FAIL [want-attrib=%s got=%s] %s\n' "$1" "${got:-<none>}" "$2" >&2; fi
|
|
182
|
+
}
|
|
183
|
+
attrib_in_session 'gh' 'glab mr list --state opened && gh pr list --state open' "$m"
|
|
184
|
+
probe quiet 'glab mr list --state opened && gh pr list --state open' "$m"
|
|
185
|
+
|
|
186
|
+
# --- UNSAFE TEMP ROOT: the marker TOCTOU cannot be closed from bash, so where
|
|
187
|
+
# the temp root is group/world-writable and NOT sticky the hook must decline
|
|
188
|
+
# to dedup rather than pretend the window is shut. Sticky is what makes a
|
|
189
|
+
# shared /tmp safe; a per-user temp root is not other-writable at all, so
|
|
190
|
+
# neither normal case is affected. ---
|
|
191
|
+
unsafe_root=$(mktemp -d "${MARKER_DIR}/unsafe-XXXXXX")
|
|
192
|
+
chmod 777 "$unsafe_root"; chmod -t "$unsafe_root" 2>/dev/null || true
|
|
193
|
+
u_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
194
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'unsafe-sess' \
|
|
195
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$u_in"
|
|
196
|
+
u1=$(TMPDIR="$unsafe_root" bash "$HOOK" < "$u_in" 2>/dev/null)
|
|
197
|
+
u2=$(TMPDIR="$unsafe_root" bash "$HOOK" < "$u_in" 2>/dev/null)
|
|
198
|
+
rm -f "$u_in"
|
|
199
|
+
if printf '%s' "$u1" | grep -q additionalContext && printf '%s' "$u2" | grep -q additionalContext; then
|
|
200
|
+
pass=$((pass+1))
|
|
201
|
+
else
|
|
202
|
+
fail=$((fail+1)); printf 'FAIL [unsafe temp root: dedup was applied instead of skipped]\n' >&2
|
|
203
|
+
fi
|
|
204
|
+
if [ ! -e "$unsafe_root/ccl-skills-cliflag" ]; then pass=$((pass+1))
|
|
205
|
+
else fail=$((fail+1)); printf 'FAIL [unsafe temp root: marker directory was created]\n' >&2; fi
|
|
206
|
+
|
|
207
|
+
# Sticky exempts a writable directory only when we or root own it, so a
|
|
208
|
+
# self-owned sticky temp root must STILL dedup (the common shared-/tmp shape).
|
|
209
|
+
own_sticky=$(mktemp -d "${MARKER_DIR}/own-XXXXXX"); chmod 1777 "$own_sticky"
|
|
210
|
+
o_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
211
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'own-sess' \
|
|
212
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$o_in"
|
|
213
|
+
o1=$(TMPDIR="$own_sticky" bash "$HOOK" < "$o_in" 2>/dev/null)
|
|
214
|
+
o2=$(TMPDIR="$own_sticky" bash "$HOOK" < "$o_in" 2>/dev/null)
|
|
215
|
+
rm -f "$o_in"
|
|
216
|
+
if printf '%s' "$o1" | grep -q additionalContext && ! printf '%s' "$o2" | grep -q additionalContext; then
|
|
217
|
+
pass=$((pass+1))
|
|
218
|
+
else
|
|
219
|
+
fail=$((fail+1)); printf 'FAIL [self-owned sticky temp root: dedup was skipped]\n' >&2
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# The walk must use the PHYSICAL path: `dirname` on a logical path resolves
|
|
223
|
+
# neither symlinks nor `..`, so a TMPDIR that merely POINTS at a hostile-ancestor
|
|
224
|
+
# directory walked a chain that does not exist on disk.
|
|
225
|
+
sym_parent=$(mktemp -d "${MARKER_DIR}/sym-XXXXXX")
|
|
226
|
+
chmod 777 "$sym_parent"; chmod -t "$sym_parent" 2>/dev/null || true
|
|
227
|
+
sym_real="$sym_parent/inner"; mkdir -p "$sym_real"; chmod 700 "$sym_real"
|
|
228
|
+
sym_link="${MARKER_DIR}/symlink-root-$$"; ln -s "$sym_real" "$sym_link"
|
|
229
|
+
s_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
230
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'sym-sess' \
|
|
231
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$s_in"
|
|
232
|
+
s1=$(TMPDIR="$sym_link" bash "$HOOK" < "$s_in" 2>/dev/null)
|
|
233
|
+
s2=$(TMPDIR="$sym_link" bash "$HOOK" < "$s_in" 2>/dev/null)
|
|
234
|
+
rm -f "$s_in"
|
|
235
|
+
if printf '%s' "$s1" | grep -q additionalContext && printf '%s' "$s2" | grep -q additionalContext; then
|
|
236
|
+
pass=$((pass+1))
|
|
237
|
+
else
|
|
238
|
+
fail=$((fail+1)); printf 'FAIL [symlinked temp root to hostile ancestor: dedup was applied]\n' >&2
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# A mode-0700 temp root under a world-writable NON-STICKY parent is swappable
|
|
242
|
+
# wholesale, so checking only the temp root itself proves nothing about the path.
|
|
243
|
+
anc_parent=$(mktemp -d "${MARKER_DIR}/anc-XXXXXX")
|
|
244
|
+
chmod 777 "$anc_parent"; chmod -t "$anc_parent" 2>/dev/null || true
|
|
245
|
+
anc_root="$anc_parent/inner"; mkdir -p "$anc_root"; chmod 700 "$anc_root"
|
|
246
|
+
a_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
247
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'anc-sess' \
|
|
248
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$a_in"
|
|
249
|
+
a1=$(TMPDIR="$anc_root" bash "$HOOK" < "$a_in" 2>/dev/null)
|
|
250
|
+
a2=$(TMPDIR="$anc_root" bash "$HOOK" < "$a_in" 2>/dev/null)
|
|
251
|
+
rm -f "$a_in"
|
|
252
|
+
if printf '%s' "$a1" | grep -q additionalContext && printf '%s' "$a2" | grep -q additionalContext \
|
|
253
|
+
&& [ ! -e "$anc_root/ccl-skills-cliflag" ]; then
|
|
254
|
+
pass=$((pass+1))
|
|
255
|
+
else
|
|
256
|
+
fail=$((fail+1)); printf 'FAIL [unsafe ANCESTOR of temp root: dedup was applied]\n' >&2
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# --- BOUNDED INPUT: past the 1 MiB read cap the payload is ignored entirely.
|
|
260
|
+
# Deterministic, not a timing bound: a timing probe could not fail at a
|
|
261
|
+
# CI-safe size, so it was removed rather than shipped. ---
|
|
262
|
+
big_in=$(mktemp "${MARKER_DIR}/big-XXXXXX")
|
|
263
|
+
awk 'BEGIN{printf "{\"tool_input\":{\"command\":\"glab mr list --state opened "; for(i=0;i<2200000;i++)printf "x"; printf "\"},\"session_id\":\"bigpay\",\"cwd\":\"/tmp\"}"}' > "$big_in"
|
|
264
|
+
big_out=$(bash "$HOOK" < "$big_in" 2>/dev/null); big_rc=$?
|
|
265
|
+
if [ "$big_rc" -ne 0 ]; then
|
|
266
|
+
fail=$((fail+1)); printf 'FAIL [oversized payload: rc=%s]\n' "$big_rc" >&2
|
|
267
|
+
elif printf '%s' "$big_out" | grep -q additionalContext; then
|
|
268
|
+
fail=$((fail+1)); printf 'FAIL [oversized payload parsed past the 1MiB read cap]\n' >&2
|
|
269
|
+
else pass=$((pass+1)); fi
|
|
270
|
+
rm -f "$big_in"
|
|
271
|
+
|
|
272
|
+
# --- MARKER SAFETY. Every attack here was reproduced first-hand before its fix.
|
|
273
|
+
# Each plant targets BOTH the current directory layout AND the pre-hardening
|
|
274
|
+
# flat path: planting only at the current path made this whole group vacuous
|
|
275
|
+
# (a mutation reverting the layout passed all of it). ---
|
|
276
|
+
FLAT='ccl-skills-cliflag-hostile-sess'
|
|
277
|
+
DIRP='ccl-skills-cliflag'
|
|
278
|
+
|
|
279
|
+
run_bounded() { # run_bounded <seconds> <input-file> <cmd...>
|
|
280
|
+
# `timeout` is NOT installed on stock macOS; calling it there returns 127 and
|
|
281
|
+
# would fail every probe here on a Darwin runner with a correct hook.
|
|
282
|
+
local limit="$1" infile="$2"; shift 2
|
|
283
|
+
local outfile pid waited=0
|
|
284
|
+
outfile=$(mktemp "${MARKER_DIR}/bounded-XXXXXX")
|
|
285
|
+
"$@" <"$infile" >"$outfile" 2>/dev/null & pid=$!
|
|
286
|
+
while kill -0 "$pid" 2>/dev/null && [ "$waited" -lt "$limit" ]; do sleep 1; waited=$((waited+1)); done
|
|
287
|
+
if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; BOUNDED_RC=124
|
|
288
|
+
else wait "$pid"; BOUNDED_RC=$?; fi
|
|
289
|
+
BOUNDED_OUT=$(cat "$outfile"); rm -f "$outfile"
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
hostile() { # hostile <label> <plant-fn>; publishes HOSTILE_SANDBOX
|
|
293
|
+
local label="$1" plant="$2" sandbox infile start elapsed
|
|
294
|
+
sandbox=$(mktemp -d "${MARKER_DIR}/hostile-XXXXXX") || { fail=$((fail+1)); return; }
|
|
295
|
+
"$plant" "$sandbox"
|
|
296
|
+
infile=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
297
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'hostile-sess' \
|
|
298
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$infile"
|
|
299
|
+
start=$(date +%s)
|
|
300
|
+
TMPDIR="$sandbox" run_bounded 8 "$infile" bash "$HOOK"
|
|
301
|
+
elapsed=$(( $(date +%s) - start )); rm -f "$infile"
|
|
302
|
+
if [ "$BOUNDED_RC" -ne 0 ] || [ "$elapsed" -ge 5 ]; then
|
|
303
|
+
fail=$((fail+1)); printf 'FAIL [hostile %s: rc=%s elapsed=%ss]\n' "$label" "$BOUNDED_RC" "$elapsed" >&2
|
|
304
|
+
elif ! printf '%s' "$BOUNDED_OUT" | grep -q 'additionalContext'; then
|
|
305
|
+
fail=$((fail+1)); printf 'FAIL [hostile %s: advisory suppressed]\n' "$label" >&2
|
|
306
|
+
else pass=$((pass+1)); fi
|
|
307
|
+
HOSTILE_SANDBOX="$sandbox"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
victim_intact() { # victim_intact <label>
|
|
311
|
+
if [ -f "$HOSTILE_SANDBOX/victim.txt" ] \
|
|
312
|
+
&& [ "$(cat "$HOSTILE_SANDBOX/victim.txt")" = 'original content' ]; then
|
|
313
|
+
pass=$((pass+1))
|
|
314
|
+
else
|
|
315
|
+
fail=$((fail+1)); printf 'FAIL [hostile %s: victim file was written through]\n' "$1" >&2
|
|
316
|
+
fi
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
plant_symlink() { echo 'original content' > "$1/victim.txt"
|
|
320
|
+
ln -s "$1/victim.txt" "$1/$DIRP"; ln -s "$1/victim.txt" "$1/$FLAT"; }
|
|
321
|
+
plant_fifo() { mkfifo "$1/$DIRP" 2>/dev/null || true; mkfifo "$1/$FLAT" 2>/dev/null || true; }
|
|
322
|
+
# NOT an ownership probe: it creates the directory as the SAME uid, so `-O`
|
|
323
|
+
# stays true and that rejection is never reached. What it covers is a
|
|
324
|
+
# PRE-EXISTING marker directory being reused without incident, plus the flat
|
|
325
|
+
# hostile object. Cross-uid `-O` has no executable probe here.
|
|
326
|
+
plant_preexisting(){ mkdir -p "$1/$DIRP"; chmod 700 "$1/$DIRP"; mkfifo "$1/$FLAT" 2>/dev/null || true; }
|
|
327
|
+
plant_inner_fifo() { mkdir -p -m 700 "$1/$DIRP"; mkfifo "$1/$DIRP/hostile-sess" 2>/dev/null || true
|
|
328
|
+
mkfifo "$1/$FLAT" 2>/dev/null || true; }
|
|
329
|
+
plant_inner_link() { mkdir -p -m 700 "$1/$DIRP"; echo 'original content' > "$1/victim.txt"
|
|
330
|
+
ln -s "$1/victim.txt" "$1/$DIRP/hostile-sess"; ln -s "$1/victim.txt" "$1/$FLAT"; }
|
|
331
|
+
# A HARD LINK passes -f, -O and the symlink test alike; reachable when the marker
|
|
332
|
+
# directory pre-existed group/world-writable.
|
|
333
|
+
plant_inner_hard() { mkdir -p "$1/$DIRP"; chmod 777 "$1/$DIRP"
|
|
334
|
+
echo 'original content' > "$1/victim.txt"
|
|
335
|
+
ln "$1/victim.txt" "$1/$DIRP/hostile-sess"
|
|
336
|
+
ln "$1/victim.txt" "$1/$FLAT" 2>/dev/null || true; }
|
|
337
|
+
|
|
338
|
+
hostile 'symlink at marker dir' plant_symlink; victim_intact 'symlink at marker dir'
|
|
339
|
+
hostile 'FIFO at marker dir' plant_fifo
|
|
340
|
+
hostile 'pre-existing marker dir' plant_preexisting
|
|
341
|
+
hostile 'FIFO as marker file' plant_inner_fifo
|
|
342
|
+
hostile 'symlink as marker file' plant_inner_link; victim_intact 'symlink as marker file'
|
|
343
|
+
hostile 'hard link as marker file' plant_inner_hard; victim_intact 'hard link as marker file'
|
|
344
|
+
|
|
345
|
+
# A pre-existing world-writable marker directory must be TIGHTENED, not merely
|
|
346
|
+
# accepted: `mkdir -p -m 700` does not touch an existing directory's mode.
|
|
347
|
+
mode_sb=$(mktemp -d "${MARKER_DIR}/mode-XXXXXX")
|
|
348
|
+
mkdir -p "$mode_sb/$DIRP"; chmod 777 "$mode_sb/$DIRP"
|
|
349
|
+
mode_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
350
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'mode-sess' \
|
|
351
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$mode_in"
|
|
352
|
+
TMPDIR="$mode_sb" run_bounded 8 "$mode_in" bash "$HOOK"; rm -f "$mode_in"
|
|
353
|
+
if [ "$BOUNDED_RC" -eq 0 ] && [ "$(ls -ld "$mode_sb/$DIRP" | cut -c1-10)" = 'drwx------' ]; then
|
|
354
|
+
pass=$((pass+1))
|
|
355
|
+
else
|
|
356
|
+
fail=$((fail+1)); printf 'FAIL [world-writable marker dir not tightened]\n' >&2
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# An OVERSIZED marker must be neither read nor written, in BOTH shapes. The size
|
|
360
|
+
# cap was once removed on the argument that the line-count bound already covered
|
|
361
|
+
# this — an argument checked only against the many-line shape, where it holds. A
|
|
362
|
+
# single enormous LINE has a line count of 1, sails past that bound, and gets
|
|
363
|
+
# appended to. Probing one dimension and concluding over both is the mistake;
|
|
364
|
+
# both shapes are asserted here so the removal cannot recur on the same reasoning.
|
|
365
|
+
big_sb=$(mktemp -d "${MARKER_DIR}/bigm-XXXXXX")
|
|
366
|
+
mkdir -p -m 700 "$big_sb/ccl-skills-cliflag"
|
|
367
|
+
awk 'BEGIN{for(i=0;i<5000;i++)print "junk"}' > "$big_sb/ccl-skills-cliflag/big-sess"
|
|
368
|
+
# ...and the single-enormous-line shape, which the line-count bound does not see.
|
|
369
|
+
awk 'BEGIN{for(i=0;i<300000;i++)printf "x"; print ""}' > "$big_sb/ccl-skills-cliflag/one-sess"
|
|
370
|
+
b_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
371
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'big-sess' \
|
|
372
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$b_in"
|
|
373
|
+
b1=$(TMPDIR="$big_sb" bash "$HOOK" < "$b_in" 2>/dev/null)
|
|
374
|
+
b2=$(TMPDIR="$big_sb" bash "$HOOK" < "$b_in" 2>/dev/null)
|
|
375
|
+
rm -f "$b_in"
|
|
376
|
+
if printf '%s' "$b1" | grep -q additionalContext && printf '%s' "$b2" | grep -q additionalContext \
|
|
377
|
+
&& [ "$(wc -l < "$big_sb/ccl-skills-cliflag/big-sess" | tr -d ' ')" = "5000" ]; then
|
|
378
|
+
pass=$((pass+1))
|
|
379
|
+
else
|
|
380
|
+
fail=$((fail+1)); printf 'FAIL [oversized many-line marker: appended to or dedup applied]\n' >&2
|
|
381
|
+
fi
|
|
382
|
+
o_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
383
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'one-sess' \
|
|
384
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$o_in"
|
|
385
|
+
o1=$(TMPDIR="$big_sb" bash "$HOOK" < "$o_in" 2>/dev/null)
|
|
386
|
+
rm -f "$o_in"
|
|
387
|
+
if printf '%s' "$o1" | grep -q additionalContext \
|
|
388
|
+
&& [ "$(wc -l < "$big_sb/ccl-skills-cliflag/one-sess" | tr -d ' ')" = "1" ]; then
|
|
389
|
+
pass=$((pass+1))
|
|
390
|
+
else
|
|
391
|
+
fail=$((fail+1)); printf 'FAIL [oversized single-line marker: was appended to]\n' >&2
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# An EMPTY marker: `grep -c ''` prints 0 but exits 1, which produced "0\n0" and a
|
|
395
|
+
# stderr diagnostic from the numeric test.
|
|
396
|
+
empty_sb=$(mktemp -d "${MARKER_DIR}/empty-XXXXXX")
|
|
397
|
+
mkdir -p -m 700 "$empty_sb/$DIRP"; : > "$empty_sb/$DIRP/probe-empty"
|
|
398
|
+
PROBE_ERR=$(mktemp "${MARKER_DIR}/err-XXXXXX")
|
|
399
|
+
e_out=$(TMPDIR="$empty_sb" jq -nc --arg c 'glab mr list --state opened' --arg s 'probe-empty' \
|
|
400
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' \
|
|
401
|
+
| TMPDIR="$empty_sb" bash "$HOOK" 2>"$PROBE_ERR"); e_rc=$?
|
|
402
|
+
contract_check "$e_out" "$e_rc" 'empty marker file'
|
|
403
|
+
rm -f "$PROBE_ERR"; PROBE_ERR=""
|
|
404
|
+
|
|
405
|
+
# A marker that is owned and single-link but NOT WRITABLE: the append is
|
|
406
|
+
# attempted and must fail SILENTLY. This is the probe that covers the subshell —
|
|
407
|
+
# an explicit `-w` check used to short-circuit here, which is why removing the
|
|
408
|
+
# subshell used to change nothing observable.
|
|
409
|
+
# Skipped as root: `chmod 400` does not stop uid 0 from appending, so under a
|
|
410
|
+
# container CI running as root this probe would pass without exercising anything.
|
|
411
|
+
# Recording the skip beats a vacuous pass.
|
|
412
|
+
if [ "$(id -u)" = "0" ]; then
|
|
413
|
+
printf 'SKIP [read-only marker probe: running as root, chmod 400 is not a barrier]\n' >&2
|
|
414
|
+
else
|
|
415
|
+
ro_sb=$(mktemp -d "${MARKER_DIR}/ro-XXXXXX")
|
|
416
|
+
mkdir -p -m 700 "$ro_sb/$DIRP"; : > "$ro_sb/$DIRP/ro-sess"; chmod 400 "$ro_sb/$DIRP/ro-sess"
|
|
417
|
+
PROBE_ERR=$(mktemp "${MARKER_DIR}/err-XXXXXX")
|
|
418
|
+
r_out=$(TMPDIR="$ro_sb" jq -nc --arg c 'glab mr list --state opened' --arg s 'ro-sess' \
|
|
419
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' \
|
|
420
|
+
| TMPDIR="$ro_sb" bash "$HOOK" 2>"$PROBE_ERR"); r_rc=$?
|
|
421
|
+
contract_check "$r_out" "$r_rc" 'read-only marker file'
|
|
422
|
+
rm -f "$PROBE_ERR"; PROBE_ERR=""
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# An over-long session id became an over-long filename and the failed open
|
|
426
|
+
# printed `File name too long` to real stderr.
|
|
427
|
+
long_sess=$(awk 'BEGIN{for(i=0;i<400;i++)printf "s"}')
|
|
428
|
+
probe remind 'glab mr list --state opened' "$long_sess"
|
|
429
|
+
# ...and the capped name must still DEDUP; without the cap the marker can never
|
|
430
|
+
# be written and the advisory repeats forever.
|
|
431
|
+
probe quiet 'glab ci list --branch dev' "$long_sess"
|
|
432
|
+
|
|
433
|
+
# A TMPDIR that looks like an OPTION (`-P`, `-L`) must not send `cd` to HOME and
|
|
434
|
+
# plant the marker directory there.
|
|
435
|
+
# NEVER delete anything this suite did not create. An earlier version removed
|
|
436
|
+
# `$HOME/ccl-skills-cliflag` on failure — and this suite runs in `make test`, so
|
|
437
|
+
# a developer whose real marker directory happened to sit there would have had it
|
|
438
|
+
# destroyed by a test run. If the path already exists, the probe cannot attribute
|
|
439
|
+
# what it finds and SKIPS rather than guess.
|
|
440
|
+
if [ -e "$HOME/ccl-skills-cliflag" ]; then
|
|
441
|
+
printf 'SKIP [option-shaped TMPDIR probe: %s pre-exists, refusing to touch it]\n' \
|
|
442
|
+
"$HOME/ccl-skills-cliflag" >&2
|
|
443
|
+
else
|
|
444
|
+
opt_in=$(mktemp "${MARKER_DIR}/in-XXXXXX")
|
|
445
|
+
jq -nc --arg c 'glab mr list --state opened' --arg s 'opt-sess' \
|
|
446
|
+
'{tool_input:{command:$c},session_id:$s,cwd:"/tmp"}' > "$opt_in"
|
|
447
|
+
TMPDIR=-P bash "$HOOK" < "$opt_in" >/dev/null 2>&1
|
|
448
|
+
TMPDIR=-L bash "$HOOK" < "$opt_in" >/dev/null 2>&1
|
|
449
|
+
rm -f "$opt_in"
|
|
450
|
+
if [ ! -e "$HOME/ccl-skills-cliflag" ]; then
|
|
451
|
+
pass=$((pass+1))
|
|
452
|
+
else
|
|
453
|
+
fail=$((fail+1))
|
|
454
|
+
# DO NOT REMOVE IT. "It was absent a moment ago, so this probe must have made
|
|
455
|
+
# it" is an inference, not attribution — a concurrent process can create the
|
|
456
|
+
# path inside that window, and this suite runs in `make test` on machines
|
|
457
|
+
# that are doing other things. A test does not delete anything it did not
|
|
458
|
+
# certainly create; reporting the path and leaving it is the correct end.
|
|
459
|
+
printf 'FAIL [option-shaped TMPDIR: %s now exists; NOT removed, inspect it yourself]\n' \
|
|
460
|
+
"$HOME/ccl-skills-cliflag" >&2
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
# --- PRESENCE, NOT BEHAVIOR. Two guards cannot be reached by any portable probe
|
|
465
|
+
# here (both need a second user id), and this suite says so. That honesty has
|
|
466
|
+
# a cost the review named: a later change could delete either one and stay
|
|
467
|
+
# green. These checks close only that half — they assert the guard is still
|
|
468
|
+
# WRITTEN, never that it works. A green here is not evidence of behavior, and
|
|
469
|
+
# must not be cited as if it were. If a guard is deliberately removed, delete
|
|
470
|
+
# its check in the same change and say why in the header. ---
|
|
471
|
+
for guard in '-O "$marker_dir"' '"$dir_owner" != "root"'; do
|
|
472
|
+
# `--` because the first guard string starts with `-O`, which grep would
|
|
473
|
+
# otherwise take as an option — the check then always failed, on a clean hook.
|
|
474
|
+
if grep -Fq -- "$guard" "$HOOK"; then
|
|
475
|
+
pass=$((pass+1))
|
|
476
|
+
else
|
|
477
|
+
fail=$((fail+1))
|
|
478
|
+
printf 'FAIL [unprobed guard vanished from the hook: %s]\n' "$guard" >&2
|
|
479
|
+
fi
|
|
480
|
+
done
|
|
481
|
+
|
|
482
|
+
printf '\n%s: pass=%d fail=%d\n' "$(basename "$0")" "$pass" "$fail"
|
|
483
|
+
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -54,6 +54,7 @@ export const OPENCODE_HOOK_BINDINGS = Object.freeze({
|
|
|
54
54
|
"guard-edit-isolation.sh": "tool.execute.before:edit/write/apply_patch",
|
|
55
55
|
"owner-dispatch-guard.sh": "tool.execute.before:edit/write/apply_patch/bash",
|
|
56
56
|
"guard-merge-authorization.sh": "tool.execute.before:bash",
|
|
57
|
+
"remind-unverified-cli-flag.sh": "tool.execute.before:bash",
|
|
57
58
|
"guard-delegation-owner.sh": "tool.execute.before:task/agent",
|
|
58
59
|
"remind-post-merge-cleanup.sh": "tool.execute.after:bash",
|
|
59
60
|
"merge-authorization-prompt.sh": "chat.message",
|
|
@@ -482,6 +483,10 @@ export const CclSkills = async (context: {
|
|
|
482
483
|
throw new Error(`ccl-skills merge authorization blocked this operation.\n${merge.message ?? "OpenCode hook runtime unavailable."}`)
|
|
483
484
|
}
|
|
484
485
|
enforce(merge, "ccl-skills merge authorization")
|
|
486
|
+
// Advisory only: it never blocks, so its context is appended rather than
|
|
487
|
+
// enforced. Mirrors hooks/remind-unverified-cli-flag.sh under Claude Code.
|
|
488
|
+
const flagNote = additionalContext(runHook(hooksRoot, "remind-unverified-cli-flag.sh", hookPayload, directory, 10_000))
|
|
489
|
+
if (flagNote) prependTaskContext(args, [flagNote])
|
|
485
490
|
}
|
|
486
491
|
|
|
487
492
|
if (tool === "task" || tool === "agent") {
|
|
@@ -25,7 +25,7 @@ Before editing app code, native project files, platform configs, styles, assets,
|
|
|
25
25
|
|
|
26
26
|
Repo-local agent contracts (`AGENTS.md` at the repo root and in source directories) are part of the delivery contract: when a change moves a stable boundary, generated surface, workflow, or directory-local rule, update the nearest contract in the same MR and keep coverage in sync per `product-rd-workflow`'s spec / repo-contract sync gate.
|
|
27
27
|
|
|
28
|
-
When checking a cross-platform app against team standards, split conformance into deterministic and agent review evidence. Deterministic checks cover build flavors/schemes, package locks, generated API/IDL client usage, environment/lane config, unit/widget/instrumented/E2E commands, CI gates, signing/release config, and request/trace identifiers in central clients. Agent review checks cover screen/module boundaries, platform abstraction leakage, native bridge contracts, finite-value mapping, device evidence quality, and whether tests prove the shipped runtime paths rather than only mocked widgets. For the per-substack deterministic executor list (each analyzer
|
|
28
|
+
When checking a cross-platform app against team standards, split conformance into deterministic and agent review evidence. Deterministic checks cover build flavors/schemes, package locks, generated API/IDL client usage, environment/lane config, unit/widget/instrumented/E2E commands, CI gates, signing/release config, and request/trace identifiers in central clients. Agent review checks cover screen/module boundaries, platform abstraction leakage, native bridge contracts, finite-value mapping, device evidence quality, and whether tests prove the shipped runtime paths rather than only mocked widgets. For the per-substack deterministic executor list (each analyzer ships thin defaults) and the shipped client conformance checkers, see `testing-strategy/references/fitness-functions.md` §4.1.3 (spec 006). Layering/dependency-direction has no out-of-box rule on any substack — express it as import-ban config (Konsist/ArchUnit/custom_lint/dep-cruiser).
|
|
29
29
|
|
|
30
30
|
1. Resolve the target app shape.
|
|
31
31
|
- Flutter shared app, React Native app, native Android, native iOS, or mixed native plus shared module.
|
|
@@ -138,6 +138,7 @@ When checking a cross-platform app against team standards, split conformance int
|
|
|
138
138
|
|
|
139
139
|
7. Verify on rendered surfaces.
|
|
140
140
|
- Run the repo's formatter, analyzer/linter, typecheck/build, and focused tests.
|
|
141
|
+
- Test-code authoring: pick the matching § from the decision table in `testing-strategy/references/test-code-authoring-patterns.md`; smell lint: `testing-strategy/references/fitness-functions.md` §4.1.4.
|
|
141
142
|
- **TC traceability**: Flutter/Dart tests link via `tcTest(['TC-XX-NNN'], 'desc', () { ... })` wrapper (registers at registration time, so `skip:` / `skipIf:` etc. still register correctly). Helper from `test-artifact-management/references/tc_helpers/tc.dart`, installed under `test/tc.dart`. Native Android/iOS use the language-appropriate wrapper convention (write a small helper that appends to `test/results/tc-map.jsonl`). See `test-artifact-management/references/tc-marker-conventions.md`. Before adding tests, `grep -rn 'tcTest.*"TC-[A-Z]' test/` plus the sidecar to check for existing coverage — extend rather than duplicate. When a TC is marked 废弃, grep both source and sidecar for that TC ID; follow deprecation cascade in `testing-strategy`. Tests without any `tcTest(...)` / equivalent helper call: prompt user only when the underlying code is also removed.
|
|
142
143
|
- **废弃级联:业务代码是否仍在用** — 每端原生 + 跨端各有套路:
|
|
143
144
|
1. Flutter / Dart:`grep -rEn "import ['\"]package:<pkg>/<file>|import ['\"]\.+/<file>" lib/`;运行 `dart analyze` 看 unused-import warning;路由表 `MaterialApp.routes` / GoRouter 配置内未注册的页面可删
|
package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: go-microservice-architecture
|
|
3
|
-
description:
|
|
3
|
+
description: Design, review, or explain a new product's Go backend/microservice architecture using Kitex or similar RPC, Hertz or similar HTTP gateway, protobuf IDL, MySQL/GORM storage, Redis cache/locks/limits, service discovery, dynamic config, message queues, observability, DI, and code generation. Also owns multi-tenant isolation, event-driven/Kafka, and data-platform (sharding/replicas/HA) architecture. Product-agnostic; no dependence on existing codebase paths, service names, or legacy repos. Prefer this for architecture/boundary/contract/data-ownership/reliability/security/platform decisions with no code changes; use go-microservice-dev for implementation. Triggers include "Go 后端架构怎么设计", "Go 微服务怎么拆", "RPC 接口怎么定义", "Go 服务边界", "Go 服务重构 / 架构分层重构", "拆分 Go 服务的上帝类/模块边界 / decompose a god class".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Go Microservice Architecture
|
|
@@ -101,6 +101,8 @@ Not appropriate for:
|
|
|
101
101
|
|
|
102
102
|
## Dependency Direction
|
|
103
103
|
|
|
104
|
+
This layering is the **[Ports-and-Adapters / Hexagonal](https://alistair.cockburn.us/hexagonal-architecture/)** idea in moderation (Alistair Cockburn; borrowed scope: the ports/adapters placement idea only, not the full pattern vocabulary) — infrastructure adapters sit behind interfaces the inner layers own.
|
|
105
|
+
|
|
104
106
|
Recommended:
|
|
105
107
|
|
|
106
108
|
```text
|
|
@@ -4,7 +4,7 @@ Use when designing the data-platform substrate of a service or service-fleet: DB
|
|
|
4
4
|
|
|
5
5
|
This complements `data-modeling-and-migrations.md` (which owns schema, index, transaction, outbox, and per-service migration concerns): this file owns the **substrate** that schema and queries sit on. Load both when designing a new data-bound service or auditing an existing one.
|
|
6
6
|
|
|
7
|
-
> **Sibling sync.** A parallel `python-service-architecture/references/data-platform-architecture.md` mirrors **all non-stack-specific sections** of this file. Only the *Go-specific implementation patterns* section diverges by stack. The mirrored sections stay free of three categories of stack-specific token: DB-engine-specific syntax, runtime/concurrency-mechanic names, and library/framework API names. The concrete token list and grep command live in the *Mirrored-section grep gate* subsection at the end of this file's stack-glue.
|
|
7
|
+
> **Sibling sync.** A parallel `python-service-architecture/references/data-platform-architecture.md` mirrors **all non-stack-specific sections** of this file. Only the *Go-specific implementation patterns* section diverges by stack. The mirrored sections stay free of three categories of stack-specific token: DB-engine-specific syntax, runtime/concurrency-mechanic names, and library/framework API names. The concrete token list and grep command live in the *Mirrored-section grep gate* subsection at the end of this file's stack-glue. Tree-specific routing references are written inline for both trees so the mirrored bytes stay identical; cross-file parity is machine-checked by `skill-extraction-workflow/scripts/check-parallel-stack-parity.sh` (wired into `check-ccl-skills.sh`), which diffs the mirrored regions byte-for-byte (no normalization) and blocks on any divergence.
|
|
8
8
|
|
|
9
9
|
> **Sanitization boundary.** Vendor names (PostgreSQL, MySQL, Vitess, TiDB, CockroachDB, Aurora, Cloud Spanner, AlloyDB, Cloud SQL, DynamoDB, RDS Proxy, PgBouncer, ProxySQL, S3, Glacier, gp3, io2, etc.) below are illustrative; concrete topology choices, region names, cluster identifiers, and capacity numbers live only in the maintainer's private alias map. The sanitization audience list is positive (external / client / regulator / SOC / procurement / internal-compliance / sales-engineering / partner draft / forwardable-internal); sanitize before any document leaves the implementation team's approved audience.
|
|
10
10
|
>
|