ktg-plugin-marketplace/plugins/linkedin-studio/scripts/test-runner.sh
Kjell Tore Guttormsen 3ae8adb6ff feat(linkedin-studio): first-hour/reply-loop command with tracked state
Wire orphan agent #11 (engagement-coach) by giving it a command surface, and
add the tracked first-hour state the plan calls for (remediation Step 16).

- commands/firsthour.md (new, 27th command): post-publish first-hour /
  reply-loop sprint. Delegates plan construction to engagement-coach via
  Task (subagent_type: linkedin-studio:engagement-coach) — returns a grouped
  target list (whales/inner-circle/ICPs/new connections), 2-3 seed
  self-comments + 3-5 CEA replies in the user's voice, and a minute-by-minute
  timeline anchored to publish time. Presents timeline/targets/drafts +
  velocity checkpoints, auto-copies the drafts to clipboard, persists the
  plan, then hands off to post-feedback-monitor for the 48h window.
- hooks/scripts/state-updater.mjs: new pure mutation recordFirstHourPlan()
  mirroring updatePostTracking — additive by contract (inserts
  last_firsthour_date after last_post_date when absent, creates the
  ## First-Hour Plans section when absent, never touches existing fields).
  Section name is deliberately non-R-initial so it stays outside
  pruneContentHistory's "## Recent Posts ... (?=\n## [^R])" capture window.
  + a --record-firsthour CLI branch for parity with the other mutations.
- config/state-file.template.md: additive scalars (last_firsthour_date,
  firsthour_active) + the ## First-Hour Plans section.
- hooks/scripts/__tests__/state-updater.test.mjs: extend (existing file) with
  7 recordFirstHourPlan tests — section creation, field insertion vs in-place
  update (no duplication), round-trip non-interference, graceful empty
  defaults, changes array.
- CLAUDE.md: register the command (## Commands 26 -> 27, table row).
- scripts/test-runner.sh: EXPECT_COMMANDS 26 -> 27 (registration guard).

Verify: grep 'subagent_type: linkedin-studio:engagement-coach' commands/ ->
firsthour.md; node --test state-updater -> 26/26; full hook suite -> 83/83;
bash scripts/test-runner.sh -> exit 0 (62 passed, commands 27/27).

Plan Step 16 (Wave 4 S3).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:35:44 +02:00

216 lines
6.6 KiB
Bash
Executable file

#!/bin/bash
# LinkedIn Studio Plugin — Structure Validator
# Validates the REAL v3.1 layout: registration counts (derived dynamically),
# frontmatter shape, hook drift, and plugin.json fields. Counts are asserted
# against the declared contract below, which is kept in sync with the
# CLAUDE.md "## Agents (N)" / "## Commands (N)" headers (cross-checked here)
# and the STATE.md "Telling" block. Adding or removing an agent, command,
# reference, or skill breaks the count-equality and fails the lint — this is
# the registration guard that gates the remediation plan's later steps.
#
# The stat-consistency grep (one magnitude per algorithm effect across the
# tree) is added in remediation Step 3, after reconciliation makes it pass.
# The version-consistency grep is added in Step 21.
#
# Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
set -e
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PLUGIN_ROOT"
PASS=0
FAIL=0
WARN=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
pass() { echo -e "${GREEN}${NC} $1"; PASS=$((PASS + 1)); }
fail() { echo -e "${RED}${NC} $1"; FAIL=$((FAIL + 1)); }
warn() { echo -e "${YELLOW}${NC} $1"; WARN=$((WARN + 1)); }
# --- Declared registration contract (the "Telling" block) ---
# Source of truth: CLAUDE.md headers + STATE.md Telling. Bump these together
# with the files when adding/removing an agent, command, reference, or skill.
EXPECT_AGENTS=19
EXPECT_COMMANDS=27
EXPECT_REFS=25
EXPECT_SKILLS=6
echo "================================================"
echo "LinkedIn Studio Plugin — Structure Validator"
echo "Plugin root: $PLUGIN_ROOT"
echo "================================================"
echo ""
# --- Section 1: Core Files ---
echo "--- Core Files ---"
for f in ".claude-plugin/plugin.json" "CLAUDE.md" "CHANGELOG.md" "README.md" "config/REMEMBER.template.md"; do
if [ -f "$f" ]; then
pass "$f exists"
else
fail "$f MISSING"
fi
done
echo ""
# --- Section 2: Registration Counts (dynamic) ---
echo "--- Registration Counts ---"
AGENTS=$(ls agents/*.md 2>/dev/null | wc -l | tr -d ' ')
COMMANDS=$(ls commands/*.md 2>/dev/null | wc -l | tr -d ' ')
REFS=$(ls references/*.md 2>/dev/null | wc -l | tr -d ' ')
SKILLS=$(ls skills/*/SKILL.md 2>/dev/null | wc -l | tr -d ' ')
assert_count() {
# $1 label, $2 actual, $3 expected
if [ "$2" -eq "$3" ]; then
pass "$1: $2 (expected $3)"
else
fail "$1: $2 (expected $3) — registration drift"
fi
}
assert_count "agents/*.md" "$AGENTS" "$EXPECT_AGENTS"
assert_count "commands/*.md" "$COMMANDS" "$EXPECT_COMMANDS"
assert_count "references/*.md" "$REFS" "$EXPECT_REFS"
assert_count "skills/*/SKILL.md" "$SKILLS" "$EXPECT_SKILLS"
# Cross-check the CLAUDE.md declared headers against the contract (doc-drift guard)
DOC_AGENTS=$(grep -oE '^## Agents \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1)
DOC_COMMANDS=$(grep -oE '^## Commands \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1)
if [ "$DOC_AGENTS" = "$EXPECT_AGENTS" ]; then
pass "CLAUDE.md '## Agents ($DOC_AGENTS)' matches contract"
else
fail "CLAUDE.md agents header ($DOC_AGENTS) != contract ($EXPECT_AGENTS)"
fi
if [ "$DOC_COMMANDS" = "$EXPECT_COMMANDS" ]; then
pass "CLAUDE.md '## Commands ($DOC_COMMANDS)' matches contract"
else
fail "CLAUDE.md commands header ($DOC_COMMANDS) != contract ($EXPECT_COMMANDS)"
fi
echo ""
# --- Section 3: Agent Frontmatter ---
echo "--- Agent Frontmatter ---"
for f in agents/*.md; do
if head -1 "$f" | grep -q "^---"; then
if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then
pass "$f (frontmatter OK)"
else
fail "$f (missing name:/description:)"
fi
else
fail "$f (no YAML frontmatter)"
fi
done
echo ""
# --- Section 4: Command Frontmatter ---
echo "--- Command Frontmatter ---"
for f in commands/*.md; do
if head -1 "$f" | grep -q "^---"; then
if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then
pass "$f (frontmatter OK)"
else
fail "$f (missing name:/description:)"
fi
else
fail "$f (no YAML frontmatter)"
fi
done
echo ""
# --- Section 5: Hook Configuration (drift) ---
echo "--- Hook Configuration ---"
if [ -f "hooks/hooks.json" ]; then
pass "hooks/hooks.json exists"
if python3 hooks/scripts/compile-hooks.py --check >/dev/null 2>&1; then
pass "hooks.json matches compiled template (no drift)"
else
fail "hooks.json DRIFT — run: python3 hooks/scripts/compile-hooks.py"
fi
else
fail "hooks/hooks.json MISSING"
fi
echo ""
# --- Section 6: Plugin.json Validation ---
echo "--- Plugin.json Validation ---"
if python3 -c "
import json, sys
with open('.claude-plugin/plugin.json') as f:
data = json.load(f)
required = ['name', 'version', 'description']
missing = [field for field in required if field not in data]
if missing:
print('Missing fields:', missing)
sys.exit(1)
print('Version:', data['version'])
" 2>/dev/null; then
pass "plugin.json structure valid (name/version/description)"
else
fail "plugin.json structure invalid"
fi
echo ""
# --- Section 7: Analytics Source ---
echo "--- Analytics Source ---"
if [ -f "scripts/analytics/src/cli.ts" ]; then
pass "scripts/analytics/src/cli.ts exists"
else
fail "scripts/analytics/src/cli.ts MISSING"
fi
echo ""
# --- Section 8: Algorithm-Stat Consistency ---
echo "--- Algorithm-Stat Consistency ---"
# The single source of truth for algorithm magnitudes is
# references/algorithm-signals-reference.md. After the Phase-0 reconciliation,
# stale/competing magnitudes — and the unpublishable model brand/date — must not
# reappear anywhere else (cite the reference, do not restate). This enforces
# "one value per effect" by forbidding the known competing values from returning.
STALE_STATS='40-50%|25-40%|6\.6%|1\.92%|15x more reach|-40-60%|360Brew|January 2026'
STAT_HITS=$(grep -rnE "$STALE_STATS" references/ commands/ skills/ hooks/prompts/ CLAUDE.md README.md 2>/dev/null | grep -v 'algorithm-signals-reference' || true)
if [ -z "$STAT_HITS" ]; then
pass "no stale algorithm magnitudes / model brand outside the canonical reference"
else
fail "stale algorithm stat(s) reintroduced — cite algorithm-signals-reference.md instead:"
echo "$STAT_HITS"
fi
echo ""
# --- Summary ---
echo "================================================"
echo "RESULTS"
echo "================================================"
echo -e "${GREEN}Passed: $PASS${NC}"
echo -e "${RED}Failed: $FAIL${NC}"
echo -e "${YELLOW}Warnings: $WARN${NC}"
echo ""
if [ $FAIL -eq 0 ]; then
echo -e "${GREEN}All structural checks passed!${NC}"
exit 0
else
echo -e "${RED}$FAIL check(s) failed. Review above.${NC}"
exit 1
fi