@ccoalm/ccl-skills 0.6.2 → 0.8.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 (139) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +11 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-unverified-cli-flag.sh +309 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_unverified_cli_flag.sh +483 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +5 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +10 -8
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +1 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +1 -1
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +2 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +1 -1
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +14 -11
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +2 -2
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +5 -1
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +13 -11
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +72 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +1 -1
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +25 -9
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +1 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +1 -1
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +16 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +7 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +11 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -10
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +4 -5
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +1 -1
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +7 -1
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -5
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +5 -1
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +1 -1
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/audit-history-architecture.md +31 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +1 -1
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +7 -4
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +2 -2
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/notification-architecture.md +28 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +1 -1
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/replay-comparison-architecture.md +28 -0
  78. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/workflow-state-architecture.md +39 -0
  79. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +10 -7
  80. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +8 -0
  81. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/audit-history-patterns.md +29 -0
  82. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +16 -0
  83. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +25 -1
  84. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/notification-patterns.md +40 -0
  85. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +1 -1
  86. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/replay-comparison-patterns.md +30 -0
  87. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +48 -0
  88. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +10 -1
  89. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +2 -0
  90. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
  91. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
  92. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +142 -4
  93. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +21 -2
  94. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
  95. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  96. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +5 -4
  97. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
  98. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +69 -0
  99. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +10 -0
  100. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
  101. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
  102. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +93 -2
  103. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-parallel-stack-parity.sh +119 -0
  104. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
  105. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
  106. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
  107. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
  108. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
  109. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_parallel_stack_parity.sh +183 -0
  110. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +19 -0
  111. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
  112. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
  113. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
  114. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
  115. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
  116. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
  117. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
  118. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
  119. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
  120. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
  121. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
  122. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
  123. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
  124. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
  125. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
  126. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
  127. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +9 -6
  128. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +11 -11
  129. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
  130. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +16 -0
  131. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +1 -1
  132. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +16 -5
  133. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +5 -3
  134. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +16 -6
  135. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/self-benchmark-baseline.md +37 -0
  136. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +7 -5
  137. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
  138. package/dist/assets/release.json +275 -105
  139. package/package.json +1 -1
@@ -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") {
@@ -21,11 +21,11 @@ Use this skill for mobile app and cross-platform client engineering. It covers F
21
21
 
22
22
  ## Core Workflow
23
23
 
24
- Before editing app code, native project files, platform configs, styles, assets, or tests, complete enough analysis and planning for the change to be reviewable. Scale the plan to risk: a simple low-risk single-screen change can use a short inline plan; multi-platform, user-visible, API-visible, native-capability, release/store, bug-fix, branch/MR, unclear-risk, or high-risk work needs explicit task split, design checkpoint, target-device matrix, acceptance checks, verification commands, rollback or stop conditions, and named handoffs to design, testing, backend, release, or diagnosis skills before edits.
24
+ Before editing app code, native project files, platform configs, styles, assets, or tests, complete enough analysis and planning for the change to be reviewable. Scale the plan to risk: a simple low-risk single-screen change can use a short inline plan; multi-platform, API-visible, native-capability, release/store, bug-fix, branch/MR, unclear-risk, or high-risk work needs explicit task split, target-device matrix, acceptance checks, verification commands, rollback or stop conditions, and named handoffs to testing, backend, release, or diagnosis skills before edits. Runtime-visible work additionally consumes the canonical UI/UX delivery contract's Design brief and Test selection Phase 0 before the first implementation edit.
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 is its own toolchain with thin defaults — `dart analyze`+`flutter_lints` with `analyzer>errors:` severity escalation, detekt `coroutines`/`exceptions` inactive rules, SwiftLint opt-in safety rules, RN typed-ESLint) and the shipped client conformance checkers, see `testing-strategy/references/fitness-functions.md` §4.1.3 (client language-basics; 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).
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.
@@ -37,8 +37,8 @@ When checking a cross-platform app against team standards, split conformance int
37
37
  - Identify shared behavior versus platform-specific behavior; list which platforms must be changed and verified.
38
38
  - Read local wrappers first: Flutter/Gradle/Xcode scripts, package managers, CI jobs, test targets, flavors/schemes, and generated files.
39
39
  - If a design exists, map each visible state and interaction to code ownership before implementing.
40
- - For any visible UI change, map the design checkpoint to code ownership: aesthetic hierarchy/density, interaction flow, behavioral feedback, user psychology, safe-area/keyboard/orientation adaptation, and device screenshot acceptance. Also record `product-ui-ux-design`'s implementation-owner checkpoint before the first edit — its field list (design/stack/test owners, entry-rule evidence, rendered/device evidence status) and copy-only path are authoritative there; load the named owner skills rather than only naming them, and treat a completion claim without `captured/verified` rendered evidence as incomplete — an explicitly accepted gap closes the slice only as `pre-runtime-test ready` / handoff, never as complete/done.
41
- - For UI/UX redesign slices meeting `product-ui-ux-design`'s page-slice trigger conditions — that gate's trigger list is authoritative and must be checked, not paraphrased, whenever a screen/surface change could be a redesign, restyle, new-style declaration, structural/visual-system change, continuation, or redesigned-surface reference — apply its cross-stack page-slice gate before framework mechanics: RED-first focused assertion, IA regrouping by user intent/consequence, behavior-contract preservation, state matrix, rendered evidence, and the design verdict (`accepted` / `rejected` / `pending`; missing = `pending`, and `design-rejected` blocks complete/MR-ready/normal/draft MR per the **Rejected-surface rule**). Translate examples from other stacks into the target app runtime instead of copying Flutter-, Android-, iOS-, or Web-specific commands as the method itself.
40
+ - For every visible UI change, load `../product-ui-ux-design/references/delivery-contract.md` and consume either its full Design brief + Phase 0 or its valid low-risk copy-only record + lightweight Phase 0 before coding. The lightweight path checks semantics, accessible name, localization, rendered extent, and target render without inventing unrelated matrices; risk-bearing copy uses the full path. For full slices, map structure, state/adaptation matrices, behavior and criteria to shared/platform owners; record build target, device, safe area, keyboard, orientation, text scale, input/lifecycle, and preserved behavior. For a native WebView, this skill owns the container/bridge member while `web-react-dev` or the actual web-content owner supplies a separate content member; neither render closes the other layer.
41
+ - Before the first implementation edit, add the canonical `client_entry` defined there: local rule identifier or short quote and implementation decision, target surface/runtime, planned run/capture command, and behavior that must remain unchanged.
42
42
 
43
43
  3. Define the client boundary before coding.
44
44
  - Screens, routes, tabs, navigation stack, deep links, and back behavior.
@@ -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 配置内未注册的页面可删
@@ -145,12 +146,13 @@ When checking a cross-platform app against team standards, split conformance int
145
146
  3. iOS(Swift/ObjC):`grep -rn "import <module>\|<ClassName>" ios/`;Xcode 的 dead-code-stripping 报告;Storyboard / XIB 引用 grep `.storyboard` 与 `.xib`
146
147
  4. 边界:跨端项目(KMM / 桥接代码)每层独立判,删一层不代表另一层也死;反射 / runtime 注解(Android)/ Objective-C runtime(iOS)grep 抓不到,需运行时验证
147
148
  - Inspect the screen in a real device, emulator, simulator, preview, or captured screenshot for any visible UI change.
148
- - For UI/UX redesign evidence, include the target device/form factor, safe-area, keyboard, orientation, dynamic type/text scale, loading/empty/error/final states, and a screenshot or equivalent rendered artifact; mark each dimension covered or `N/A` with a one-line reason. `N/A` is valid only when the reason names a verifiable structural fact, explains why that fact makes the dimension unreachable or unchanged for this slice, and includes a checkable pointer such as a file path, config key, or commit that resolves at review time. Also record the design verdict (`accepted` / `rejected` / `pending`) from a user or named independent design-owner review per `product-ui-ux-design`'s page-slice gate; a missing or absent verdict counts as `pending` and blocks complete/MR-ready exactly like a missing dimension — the screenshot alone is never acceptance. Persist evidence artifacts where reviewers can access them using sanitized/test accounts and redacting tokens, PII, credentials, private paths, and raw personal data; remove temporary smoke files or scripts before commit unless the repo intentionally owns them.
149
+ - For UI/UX redesign evidence, include the target device/form factor, safe-area, keyboard, orientation, dynamic type/text scale, loading/empty/error/final states, and a screenshot or equivalent rendered artifact; mark each dimension covered or `N/A` with a one-line reason. `N/A` is valid only when the reason names a verifiable structural fact, explains why that fact makes the dimension unreachable or unchanged for this slice, and includes a checkable pointer such as a file path, config key, or commit that resolves at review time. Persist evidence artifacts where reviewers can access them using sanitized/test accounts and redacting tokens, PII, credentials, private paths, and raw personal data; remove temporary smoke files or scripts before commit unless the repo intentionally owns them.
150
+ - Return the complete canonical client-record member defined in `../product-ui-ux-design/references/delivery-contract.md` for testing Phase 1 and the design verdict. The member includes its applied rule/decision, affected files/components, preserved behavior, exact command, immutable candidate binding, producer member/version actually exercised, artifacts, tested build/device targets, dimensions/states/input and lifecycle modes, criterion-mapped observations, coverage boundary, and gaps. A screenshot proves only the captured host/member states; it cannot close an unbound producer member. `testing-strategy` records aggregate sufficiency before the design owner records the candidate-bound verdict.
149
151
  - In approval-sensitive runtimes, do not create a new one-off smoke file for every screenshot or slice. Prefer a repo-owned smoke harness, existing integration test, debug route, fixture flag, deep link, or already-created slice harness; if a temporary harness is genuinely unavoidable, reuse one stable harness for the whole batch, mutate it minimally, and explicitly delete it before commit (a throwaway harness is evidence scaffolding, not shippable code; verify it is gone in the pre-commit diff) unless the repo intentionally owns it. Repeated edit-approval prompts from throwaway helper files are an execution defect, not normal evidence collection.
150
152
  - Capture Android evidence as approval-friendly single commands. In environments where command-prefix approval is used, do not combine `adb` with shell pipes, `>`, `>>`, command substitution, `&&`, or local filtering in the same command; those forms are split or de-scoped by the approval layer and can re-trigger prompts even when `adb` itself is approved. Use sanitized/test fixture screens, then run three standalone commands: `adb -s <serial> shell screencap -p "/sdcard/<artifact>.png"`, `adb -s <serial> pull "/sdcard/<artifact>.png" "<repo-evidence-path>/"`, and `adb -s <serial> shell rm "/sdcard/<artifact>.png"` in cleanup even when pull fails or the run aborts. Generate `<artifact>` from a safe basename alphabet (no spaces, globs, or path separators) and quote the `/sdcard` path in every command so `rm` targets exactly the file you created, and create the destination with `mkdir -p <repo-evidence-path>` before pulling. Never write credential, PII, account, or otherwise sensitive real-user screens to `/sdcard`; redact or fixture first. If the environment explicitly pre-approves shell redirection for `adb`, `adb -s <serial> exec-out screencap -p > <repo-evidence-path>/<artifact>.png` is acceptable and avoids device-side storage, but the transcript must record that redirection is approved for that command shape. Either way, validate the captured file is a non-empty valid PNG before relying on it: a failed pull, or an empty/zero-byte `exec-out` stream, means there is no host artifact — retry rather than counting it as evidence.
151
- - A mobile screenshot that proves the app rendered is not automatically design acceptance evidence. If the design owner or page-slice review rejects the captured surface for weak hierarchy, blank-feeling composition, generic patched layout, clipped safe-area/keyboard behavior, or mismatch with the design checkpoint, route back to `product-ui-ux-design`'s **Rejected-surface rule** and re-capture after redesign; the app implementation waits on that design verdict instead of self-adjudicating taste. A technically successful but visually failed smoke leaves the slice `design-rejected` — blocking complete, MR-ready, and any normal or draft MR until the re-rendered surface is accepted, not only the completion claim.
153
+ - A missing or absent design verdict remains `pending` and blocks the canonical contract's `complete`, MR-ready, merge-ready, and normal MR/handoff readiness states; a mobile screenshot proves only that the app rendered and is never design acceptance. If a hierarchy, composition, behavior, safe-area/keyboard, or brief criterion fails, return that failure instead of self-adjudicating taste; the design owner applies `delivery-contract.md`'s rejected-candidate path and requires fresh runtime evidence for the revised candidate.
152
154
  - For platform enablement, verify each newly added platform with a real platform build and an install/run smoke on the relevant emulator or simulator. Adding the host project or passing one platform does not complete the slice; record the platform targets, commands, and screenshot or runtime evidence.
153
- - For mobile runtime changes, device/emulator/simulator smoke is a completion gate when lower layers cannot prove the behavior. This includes changes to native capabilities, WebView/native bridge callbacks, foreground/background recovery, orientation/safe-area/keyboard behavior, storage/session restore, route/deep-link behavior, upload/media flows, permissions, and rendered loading/error/final states. If the runner is missing, first attempt normal setup; if still unavailable, stop at `pre-runtime-test ready` or `blocked` and name the owner, attempted commands, residual risk, and next unblock action. `pre-runtime-test ready` is handoff-only, not merge-ready, release-ready, or complete.
155
+ - For mobile runtime changes, device/emulator/simulator smoke is a completion gate when lower layers cannot prove the behavior. This includes changes to native capabilities, WebView/native bridge callbacks, foreground/background recovery, orientation/safe-area/keyboard behavior, storage/session restore, route/deep-link behavior, upload/media flows, permissions, and rendered loading/error/final states. If the runner is missing, first attempt normal setup; if still unavailable, stop at `pre-runtime-test-ready` or `blocked` and name the owner, attempted commands, residual risk, and next unblock action. `pre-runtime-test-ready` is handoff-only, not merge-ready, release-ready, or complete.
154
156
  - For emulator readiness scripts, do not trust process launch or boot log text alone. Confirm the runner sees the target device, then verify direct device state such as `adb devices -l`, `adb -s <serial> get-state`, and platform boot readiness before calling Android ready; keep the serial in the output so later E2E commands target the same device.
155
157
  - If the app or app-hosted H5 CI only builds or deploys packages, treat that as structural release evidence, not functional proof. Interaction, permission, lifecycle, upload/media, and recovery changes still need focused assertions plus rendered device or host-container smoke.
156
158
  - Check accessibility labels, dynamic type/text scale, focus order, screen reader reachability, contrast, and touch targets.
@@ -174,7 +176,7 @@ When checking a cross-platform app against team standards, split conformance int
174
176
  - Do not treat a mobile API client as done until empty response, invalid JSON, non-2xx envelope, auth expiry, network failure, cancellation, and backend error message extraction are covered at the client or screen boundary when relevant.
175
177
  - Do not scatter backend enum/string literals through app screens, deep links, native bridge payload handling, storage, analytics, or tests. Centralize finite-value parsing, display labels, defaults, and unknown-value behavior at the API/client-domain boundary, and keep raw literals only in clearly named boundary conversion tests that cover every known external value plus unknown/default behavior. Migrate existing non-boundary test raw literals for that value in the same pull request or mark each remaining use with `finite-value-debt: <task-ref> <owner> <deadline> <reason>`, even when the current slice does not introduce a new mapper.
176
178
  - Do not debug app failures from code inspection alone when a runnable reproduction, trace, screenshot, or device log can be collected.
177
- - Do not claim a mobile client fix is complete without naming the platform targets that were verified. If required device/emulator/simulator smoke is unavailable after remediation, the status is `pre-runtime-test ready` or `blocked`, not complete.
179
+ - Do not claim a mobile client fix is complete without naming the platform targets that were verified. If required device/emulator/simulator smoke is unavailable after remediation, the status is `pre-runtime-test-ready` or `blocked`, not complete.
178
180
 
179
181
  ## Reference Loading
180
182
 
@@ -74,4 +74,4 @@ When the mobile app must talk to internal-CA / enterprise-CA / self-signed hosts
74
74
  - **Debug-only instrumentation must be structurally excluded from every real-user build, and the exclusion verified on the exact shipped artifact — a runtime flag is not the guard.** The class is broad: debug bridges, in-app state/inspection servers, dev overlays/menus, network inspectors, remote WebView debugging, verbose token/PII logging, mock-auth or auth-bypass hooks, and any QA state-export or runtime-introspection surface — including ones with no visible UI that are still reachable via deep link, push action, JS bridge message, or custom URL scheme. Any of these is an attack surface and data-exposure risk (an in-app state server can bind a port and dump app state on a real user's device) if it reaches a build that can touch real users, real auth, or real PII. **Scope by distribution channel + data sensitivity, not a single `isProduction`/`isDebug` enum**: the gate covers App Store / Play production AND TestFlight, enterprise / ad-hoc, dogfood, Play internal/closed testing, and staged rollout — all carry real users (same channel-not-enum boundary as **TLS Trust And Certificate Pinning** above).
75
75
  - **Exclude at the dependency/target level, per real-user variant — not just a runtime flag, and not just the variant named `release`.** An `if (isDebug)` check still compiles the code — and any bound port/server — into the binary; even `kDebugMode` / `__DEV__` only strip the Dart/JS call path while a native plugin/module/platform-channel stays linked. The primary guarantee is that the debug dependency/target is absent from the release classpath / target membership / build flavor of *every variant that reaches real users*, verified per variant: iOS `#if DEBUG` plus Debug-only target membership — CocoaPods can scope a pod to Debug configurations (`:configurations => ['Debug']`), but SwiftPM has no build-configuration-scoped *package dependency*, so a debug package must be confined to a debug-only target/scheme and left unlinked by every real-user target rather than gated by a `Package.swift` condition; also confirm no real-user scheme (TestFlight / enterprise / staging) inherits `DEBUG` in `SWIFT_ACTIVE_COMPILATION_CONDITIONS` / `GCC_PREPROCESSOR_DEFINITIONS`, or `#if DEBUG` code compiles straight into it; Android a `debugImplementation` / debug source-set dependency, NOT `implementation` plus a runtime check (a manifest `ContentProvider`/initializer or reflection keep-rule runs it anyway) — and note a `debug` / `qaDebug` buildType shipped to dogfood/internal testing IS a real-user build that includes those deps, so real-user channels must use release-class variants with debug deps excluded; Flutter/RN remove the package from the release flavor at the platform level (a native plugin autolinks/registers into the binary via podspec/Gradle/manifest unless excluded from the release target, even when the Dart/JS guard is stripped).
76
76
  - **Verify on the exact final signed/packaged upload artifact per channel and variant, and treat a symbol/string grep as a backstop, not proof.** A single `nm -j` / `strings` / dexdump / grep is defeated by R8 / Hermes / LTO renaming (`DevInspector` → `a.b.C`), dynamic loading (`dlopen`, Android dynamic feature, RN/Flutter OTA bundle, remote WebView JS), and non-code surfaces (debug assets, URL schemes, manifest providers/services, plist keys, debug entitlements). *Discover* the inventory from the dependency graph, manifest/plist, entitlements, and route/deep-link/bridge registries — not a remembered list, since an unlisted transitive SDK initializer or debug bridge handler is exactly the gap — then for each assert absence across binary, dex/native libs, assets/resources, manifest/plist, entitlements (`get-task-allow` is debug-only; Local Network / ATS / cleartext exceptions are security-sensitive plist/capability config that need production justification, not necessarily debug), and the OTA/dynamic-load channels reachable by that release (a passing artifact digest does NOT bind a debug bundle pushed later via CodePush/Expo/remote WebView JS, so real-user OTA/remote channels must not serve debug payloads). Run it on the post-signing artifact you actually upload (record its digest), across every shipped platform / scheme / flavor / extension, never on an intermediate `assembleRelease`. (`__DEV__` / `kDebugMode` elimination is also minifier-dependent and has silently regressed historically, so the source guard alone is never proof.)
77
- - **Make it a release-blocking evidence row, not MR prose** (otherwise the gate is decorative — a team ships from local Xcode/Fastlane, self-marks "checked", or never adds the CI job). The row is machine-generated, tied to the uploaded artifact digest + variant, and verified by someone other than the author; it records the digest, distribution channel, variant matrix, and check-command output, and the release/upload lane fails closed on a missing row. **Risk acceptance cannot waive structural exclusion** of debug-only instrumentation in any real-user / real-auth / real-PII build: an exception may only reclassify the channel as non-real-user / no-real-data, or apply to a production-intended diagnostic that ships its own auth + redaction controls (at which point it is no longer debug-only instrumentation) — otherwise the release stays blocked, an unbounded "accepted" is not a valid disposition. Guidance cannot force a given repo's pipeline to fail closed — that mechanical enforcement is the release-pipeline owner's job (route to `platform-release-engineering`); absent the row the status is not release-ready, the same way missing device smoke leaves a slice `pre-runtime-test ready`. A manual removal/teardown flow is a convenience path for a one-off exit, never the mechanism that keeps debug code out of real-user builds.
77
+ - **Make it a release-blocking evidence row, not MR prose** (otherwise the gate is decorative — a team ships from local Xcode/Fastlane, self-marks "checked", or never adds the CI job). The row is machine-generated, tied to the uploaded artifact digest + variant, and verified by someone other than the author; it records the digest, distribution channel, variant matrix, and check-command output, and the release/upload lane fails closed on a missing row. **Risk acceptance cannot waive structural exclusion** of debug-only instrumentation in any real-user / real-auth / real-PII build: an exception may only reclassify the channel as non-real-user / no-real-data, or apply to a production-intended diagnostic that ships its own auth + redaction controls (at which point it is no longer debug-only instrumentation) — otherwise the release stays blocked, an unbounded "accepted" is not a valid disposition. Guidance cannot force a given repo's pipeline to fail closed — that mechanical enforcement is the release-pipeline owner's job (route to `platform-release-engineering`); absent the row the status is not release-ready, the same way missing device smoke leaves a slice `pre-runtime-test-ready`. A manual removal/teardown flow is a convenience path for a one-off exit, never the mechanism that keeps debug code out of real-user builds.