create-anpunkit 2.0.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 +44 -0
- package/bin/cli.js +41 -0
- package/package.json +34 -0
- package/template/.claude/agents/debugger.md +47 -0
- package/template/.claude/agents/e2e-runner.md +63 -0
- package/template/.claude/agents/implementer.md +78 -0
- package/template/.claude/agents/infra-provisioner.md +159 -0
- package/template/.claude/agents/planner.md +78 -0
- package/template/.claude/agents/researcher.md +103 -0
- package/template/.claude/agents/synthesizer.md +74 -0
- package/template/.claude/agents/test-author.md +71 -0
- package/template/.claude/anpunkit-manifest.json +256 -0
- package/template/.claude/commands/infra.md +62 -0
- package/template/.claude/commands/log-decision.md +34 -0
- package/template/.claude/commands/log-issue.md +28 -0
- package/template/.claude/commands/overview.md +106 -0
- package/template/.claude/commands/phase.md +202 -0
- package/template/.claude/commands/quick.md +30 -0
- package/template/.claude/commands/replan.md +64 -0
- package/template/.claude/commands/store-wisdom.md +195 -0
- package/template/.claude/commands/synthesize.md +26 -0
- package/template/.claude/commands/unstuck.md +40 -0
- package/template/.claude/hooks/cursor-session-start.sh +14 -0
- package/template/.claude/hooks/pre-compact.sh +25 -0
- package/template/.claude/hooks/session-start.sh +135 -0
- package/template/.claude/hooks/subagent-stop.sh +11 -0
- package/template/.claude/settings.json +16 -0
- package/template/.claude/skills/caveman/SKILL.md +39 -0
- package/template/.claude/skills/grill-me/SKILL.md +10 -0
- package/template/.claude/skills/karpathy-guidelines/SKILL.md +34 -0
- package/template/.cursor/commands/infra.md +57 -0
- package/template/.cursor/commands/log-decision.md +29 -0
- package/template/.cursor/commands/log-issue.md +23 -0
- package/template/.cursor/commands/overview.md +102 -0
- package/template/.cursor/commands/phase.md +197 -0
- package/template/.cursor/commands/quick.md +25 -0
- package/template/.cursor/commands/replan.md +59 -0
- package/template/.cursor/commands/store-wisdom.md +191 -0
- package/template/.cursor/commands/synthesize.md +22 -0
- package/template/.cursor/commands/unstuck.md +36 -0
- package/template/.cursor/hooks.json +14 -0
- package/template/.cursor/rules/anpunkit.md +11 -0
- package/template/.gitattributes +12 -0
- package/template/AGENTS.md +216 -0
- package/template/CLAUDE.md +70 -0
- package/template/README.md +283 -0
- package/template/commands.src/infra.md +62 -0
- package/template/commands.src/log-decision.md +34 -0
- package/template/commands.src/log-issue.md +28 -0
- package/template/commands.src/overview.md +106 -0
- package/template/commands.src/phase.md +202 -0
- package/template/commands.src/quick.md +30 -0
- package/template/commands.src/replan.md +64 -0
- package/template/commands.src/store-wisdom.md +195 -0
- package/template/commands.src/synthesize.md +26 -0
- package/template/commands.src/unstuck.md +40 -0
- package/template/docker-compose.test.yml +34 -0
- package/template/docs/DESIGN_LOG.md +578 -0
- package/template/e2e/global-setup.ts +57 -0
- package/template/playwright.config.ts +28 -0
- package/template/scripts/auth-setup.sh +51 -0
- package/template/scripts/e2e-stack.sh +65 -0
- package/template/scripts/regression.sh +46 -0
- package/template/setup.sh +236 -0
- package/template/tests/phase-1/README.md +4 -0
- package/template/tests/regression/README.md +11 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/auth-setup.sh — verify Azure CLI session is ready.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
5
|
+
|
|
6
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
7
|
+
ok() { printf "${GREEN} OK${NC} %s\n" "$1"; }
|
|
8
|
+
warn() { printf "${YELLOW} WARN${NC} %s\n" "$1"; }
|
|
9
|
+
fail() { printf "${RED} FAIL${NC} %s\n" "$1"; }
|
|
10
|
+
|
|
11
|
+
echo ""
|
|
12
|
+
echo "=== Azure session check ==="
|
|
13
|
+
echo ""
|
|
14
|
+
|
|
15
|
+
if ! command -v az >/dev/null 2>&1; then
|
|
16
|
+
fail "az CLI not found. Install from https://learn.microsoft.com/cli/azure/install-azure-cli"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
AZ_VER=$(az version --query '"azure-cli"' -o tsv 2>/dev/null || echo "unknown")
|
|
20
|
+
ok "az CLI present (version: ${AZ_VER})"
|
|
21
|
+
|
|
22
|
+
ACCOUNT_JSON=$(az account show 2>/dev/null || true)
|
|
23
|
+
if [ -z "$ACCOUNT_JSON" ]; then
|
|
24
|
+
warn "Not logged in. Running az login..."
|
|
25
|
+
az login
|
|
26
|
+
ACCOUNT_JSON=$(az account show 2>/dev/null || true)
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ -z "$ACCOUNT_JSON" ]; then
|
|
30
|
+
fail "az login failed or was cancelled."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SUB_NAME=$(echo "$ACCOUNT_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['name'])" 2>/dev/null || echo "unknown")
|
|
35
|
+
SUB_ID=$(echo "$ACCOUNT_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "unknown")
|
|
36
|
+
ok "Logged in — subscription: ${SUB_NAME} (${SUB_ID})"
|
|
37
|
+
|
|
38
|
+
# Token freshness check
|
|
39
|
+
TOKEN_EXP=$(az account get-access-token --query expiresOn -o tsv 2>/dev/null || echo "")
|
|
40
|
+
if [ -n "$TOKEN_EXP" ]; then
|
|
41
|
+
ok "Token valid until: ${TOKEN_EXP}"
|
|
42
|
+
else
|
|
43
|
+
warn "Could not determine token expiry — may need re-login during session."
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Mark auth done for this session (suppresses SessionStart hook nudge)
|
|
47
|
+
PROJECT_HASH=$(echo "$PWD" | md5sum 2>/dev/null | cut -c1-8 || echo "anpunkit")
|
|
48
|
+
touch "/tmp/anpunkit-auth-${USER:-unknown}-${PROJECT_HASH}"
|
|
49
|
+
|
|
50
|
+
echo ""
|
|
51
|
+
echo "Azure session ready. You can now run /infra or Azure-dependent phases."
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/e2e-stack.sh — E2E test stack lifecycle.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
cd "$(dirname "$0")/.."
|
|
5
|
+
|
|
6
|
+
CMD="${1:-}"
|
|
7
|
+
COMPOSE="docker-compose.test.yml"
|
|
8
|
+
ENVFILE=".env.test"
|
|
9
|
+
|
|
10
|
+
[ -f "$ENVFILE" ] || { echo "missing $ENVFILE — run /infra to generate it"; exit 1; }
|
|
11
|
+
set -a; . "./$ENVFILE"; set +a
|
|
12
|
+
|
|
13
|
+
if [ "${E2E_STACK_EXTERNAL:-}" = "1" ]; then
|
|
14
|
+
echo "[e2e-stack] E2E_STACK_EXTERNAL=1 — targeting deployed Azure app at ${E2E_BASE_URL:-<unset>}"
|
|
15
|
+
case "$CMD" in
|
|
16
|
+
up)
|
|
17
|
+
echo "[e2e-stack] Verifying Azure app is reachable..."
|
|
18
|
+
for i in $(seq 1 10); do
|
|
19
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}/health" >/dev/null 2>&1 \
|
|
20
|
+
|| curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
21
|
+
echo "[e2e-stack] Azure app reachable."
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
sleep 3
|
|
25
|
+
done
|
|
26
|
+
echo "[e2e-stack] WARNING: Azure app not reachable. Classify as AZURE UNAVAILABLE."
|
|
27
|
+
exit 1
|
|
28
|
+
;;
|
|
29
|
+
down)
|
|
30
|
+
echo "[e2e-stack] External mode — nothing to stop."
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
echo "usage: e2e-stack.sh up|down"
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if docker compose version >/dev/null 2>&1; then DC="docker compose"; else DC="docker-compose"; fi
|
|
41
|
+
|
|
42
|
+
case "$CMD" in
|
|
43
|
+
up)
|
|
44
|
+
echo "[e2e-stack] building + starting local app containers..."
|
|
45
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" up -d --build
|
|
46
|
+
for i in $(seq 1 30); do
|
|
47
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
48
|
+
echo "[e2e-stack] up."
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
sleep 3
|
|
52
|
+
done
|
|
53
|
+
echo "[e2e-stack] ERROR: app did not become healthy — STACK NOT READY"
|
|
54
|
+
exit 1
|
|
55
|
+
;;
|
|
56
|
+
down)
|
|
57
|
+
echo "[e2e-stack] stopping local containers..."
|
|
58
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" down
|
|
59
|
+
echo "[e2e-stack] down."
|
|
60
|
+
;;
|
|
61
|
+
*)
|
|
62
|
+
echo "usage: e2e-stack.sh up|down"
|
|
63
|
+
exit 1
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# regression.sh — run the cross-phase regression corpus (tests/regression/).
|
|
3
|
+
# The accumulated MOCK corpus is the always-on regression guard (fast,
|
|
4
|
+
# deterministic): run on every phase CLOSE, after /quick, after /replan.
|
|
5
|
+
# The REAL corpus runs only at the final phase and after /replan.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/regression.sh # mock corpus (default)
|
|
9
|
+
# bash scripts/regression.sh --real # real corpus (hits live services)
|
|
10
|
+
#
|
|
11
|
+
# mock-vs-real is a fixture/env FLAG on the SAME test, surfaced via TEST_MODE.
|
|
12
|
+
# Tests read TEST_MODE to decide whether to mock the external boundary.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
15
|
+
|
|
16
|
+
MODE="mock"
|
|
17
|
+
[ "${1:-}" = "--real" ] && MODE="real"
|
|
18
|
+
export TEST_MODE="$MODE"
|
|
19
|
+
|
|
20
|
+
CORPUS="tests/regression"
|
|
21
|
+
if [ ! -d "$CORPUS" ]; then
|
|
22
|
+
echo "regression: no $CORPUS/ dir — nothing to guard yet."
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Count corpus files (visible, auditable — no hidden marker state).
|
|
27
|
+
COUNT=$(find "$CORPUS" -type f \( -name '*test*' -o -name '*spec*' \) 2>/dev/null | wc -l | tr -d ' ')
|
|
28
|
+
echo "=== regression ($MODE) — $COUNT corpus file(s) in $CORPUS/ ==="
|
|
29
|
+
if [ "$COUNT" = "0" ]; then
|
|
30
|
+
echo "regression: corpus empty — pass (no contracts to protect yet)."
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Pick a runner. Pytest for Python; npm test for Node. Extend as needed.
|
|
35
|
+
if find "$CORPUS" -name '*.py' | grep -q . && command -v pytest >/dev/null 2>&1; then
|
|
36
|
+
echo "runner: pytest"
|
|
37
|
+
pytest "$CORPUS" -q
|
|
38
|
+
elif [ -f package.json ] && grep -q '"test"' package.json; then
|
|
39
|
+
echo "runner: npm test ($CORPUS)"
|
|
40
|
+
TEST_MODE="$MODE" npm test -- "$CORPUS"
|
|
41
|
+
else
|
|
42
|
+
echo "regression: ERROR — corpus present but no runner detected (pytest / npm test)." >&2
|
|
43
|
+
echo "Wire a runner for tests/regression/ before relying on the guard." >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
echo "=== regression ($MODE) GREEN ==="
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# setup.sh — anpunkit installer engine (single source of truth for install logic).
|
|
3
|
+
#
|
|
4
|
+
# Two entry paths share this script:
|
|
5
|
+
# - Developers: git clone … && bash setup.sh (--src defaults to ".")
|
|
6
|
+
# - End users: npx create-anpunkit (bin runs: bash setup.sh --src <pkgdir>)
|
|
7
|
+
#
|
|
8
|
+
# Non-destructive by design. Runs non-interactively under `set -euo pipefail`
|
|
9
|
+
# (no TTY assumed). Ownership taxonomy (see README/AGENTS.md):
|
|
10
|
+
# kit-owned -> refreshed on upgrade (unless user-modified -> *.anpunkit-new)
|
|
11
|
+
# user-owned -> NEVER overwritten
|
|
12
|
+
# hybrid -> merged idempotently, never replaced
|
|
13
|
+
#
|
|
14
|
+
# Flags: --src DIR --kb-path P --kb-remote URL --no-kb --force --dry-run
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SRC="."
|
|
18
|
+
KB_PATH="${ANPUNKIT_KB_PATH:-}"
|
|
19
|
+
KB_REMOTE="${ANPUNKIT_KB_REMOTE:-}"
|
|
20
|
+
NO_KB=0
|
|
21
|
+
FORCE=0
|
|
22
|
+
DRY=0
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--src) SRC="$2"; shift 2;;
|
|
26
|
+
--kb-path) KB_PATH="$2"; shift 2;;
|
|
27
|
+
--kb-remote) KB_REMOTE="$2"; shift 2;;
|
|
28
|
+
--no-kb) NO_KB=1; shift;;
|
|
29
|
+
--force) FORCE=1; shift;;
|
|
30
|
+
--dry-run) DRY=1; shift;;
|
|
31
|
+
*) echo "unknown flag: $1" >&2; exit 2;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
DEST="$(pwd)"
|
|
36
|
+
say() { printf '%s\n' "$*"; }
|
|
37
|
+
act() { if [ "$DRY" = 1 ]; then say " [dry-run] $*"; else say " $*"; fi; }
|
|
38
|
+
|
|
39
|
+
sha() {
|
|
40
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
|
|
41
|
+
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
|
|
42
|
+
else python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
say "anpunkit setup — dest: $DEST src: $SRC $([ "$DRY" = 1 ] && echo '(DRY-RUN)')"
|
|
46
|
+
|
|
47
|
+
MAN_SRC="$SRC/.claude/anpunkit-manifest.json"
|
|
48
|
+
MAN_DEST="$DEST/.claude/anpunkit-manifest.json"
|
|
49
|
+
[ -f "$MAN_SRC" ] || { echo "FATAL: missing $MAN_SRC (corrupt kit)"; exit 1; }
|
|
50
|
+
NEW_VER=$(python3 -c "import json;print(json.load(open('$MAN_SRC'))['version'])")
|
|
51
|
+
|
|
52
|
+
# ---- determine mode from installed manifest
|
|
53
|
+
MODE="fresh"
|
|
54
|
+
if [ -f "$MAN_DEST" ]; then
|
|
55
|
+
OLD_VER=$(python3 -c "import json;print(json.load(open('$MAN_DEST')).get('version',''))" 2>/dev/null || echo "")
|
|
56
|
+
if [ "$OLD_VER" = "$NEW_VER" ]; then MODE="repair"
|
|
57
|
+
elif [ -z "$OLD_VER" ]; then MODE="upgrade"
|
|
58
|
+
else
|
|
59
|
+
# crude semver compare
|
|
60
|
+
if [ "$(printf '%s\n%s\n' "$OLD_VER" "$NEW_VER" | sort -V | tail -1)" = "$NEW_VER" ] && [ "$OLD_VER" != "$NEW_VER" ]; then MODE="upgrade"; else MODE="newer-installed"; fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
say "mode: $MODE (installed: ${OLD_VER:-none} -> new: $NEW_VER)"
|
|
64
|
+
[ "$MODE" = "newer-installed" ] && say " ! installed version is newer than this package — proceeding as repair, review carefully."
|
|
65
|
+
|
|
66
|
+
BACKUP="$DEST/.anpunkit-backup-$(date +%Y%m%d-%H%M%S)"
|
|
67
|
+
backup_one() {
|
|
68
|
+
[ "$MODE" = "fresh" ] && return 0
|
|
69
|
+
[ -f "$1" ] || return 0
|
|
70
|
+
[ "$DRY" = 1 ] && return 0
|
|
71
|
+
mkdir -p "$BACKUP/$(dirname "${1#$DEST/}")"
|
|
72
|
+
cp -p "$1" "$BACKUP/${1#$DEST/}"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# ---- per kit-owned file: write / refresh / keep-as-new
|
|
76
|
+
# Reads kit-owned list from the NEW manifest.
|
|
77
|
+
install_kit_file() {
|
|
78
|
+
local rel="$1" s="$SRC/$1" d="$DEST/$1"
|
|
79
|
+
[ -f "$s" ] || return 0
|
|
80
|
+
if [ ! -f "$d" ]; then
|
|
81
|
+
act "write $rel"; [ "$DRY" = 1 ] && return 0
|
|
82
|
+
mkdir -p "$(dirname "$d")"; cp -p "$s" "$d"; return 0
|
|
83
|
+
fi
|
|
84
|
+
# present — compare against the INSTALLED manifest's recorded checksum
|
|
85
|
+
local recorded; recorded=$(python3 - "$MAN_DEST" "$rel" <<'PY' 2>/dev/null || true
|
|
86
|
+
import json,sys
|
|
87
|
+
try:
|
|
88
|
+
m=json.load(open(sys.argv[1]))
|
|
89
|
+
print(next((f["sha256"] for f in m["files"] if f["path"]==sys.argv[2]),""))
|
|
90
|
+
except Exception: print("")
|
|
91
|
+
PY
|
|
92
|
+
)
|
|
93
|
+
local cur; cur=$(sha "$d")
|
|
94
|
+
if [ "$cur" = "$recorded" ] || [ -z "$recorded" ] && [ "$SRC" = "." ]; then
|
|
95
|
+
# unmodified since last install (or clone flow with file already in place) -> refresh
|
|
96
|
+
if [ "$cur" = "$(sha "$s")" ]; then act "ok $rel (identical)"; else
|
|
97
|
+
backup_one "$d"; act "refresh $rel"; [ "$DRY" = 1 ] || cp -p "$s" "$d"; fi
|
|
98
|
+
else
|
|
99
|
+
# user-modified kit file
|
|
100
|
+
if [ "$FORCE" = 1 ]; then backup_one "$d"; act "force $rel (overwritten)"; [ "$DRY" = 1 ] || cp -p "$s" "$d"
|
|
101
|
+
else act "keep $rel (user-modified) -> wrote $rel.anpunkit-new"; [ "$DRY" = 1 ] || cp -p "$s" "$d.anpunkit-new"; fi
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
say ""; say "[1/6] kit-owned files (taxonomy):"
|
|
106
|
+
if [ "$SRC" != "." ]; then
|
|
107
|
+
while IFS= read -r rel; do install_kit_file "$rel"; done < <(python3 -c "import json;[print(f['path']) for f in json.load(open('$MAN_SRC'))['files']]")
|
|
108
|
+
else
|
|
109
|
+
say " clone flow (src=.) — kit files already in place; verifying + generating only."
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ---- generate command adapters from commands.src/ (manifest-owned output)
|
|
113
|
+
say ""; say "[2/6] generate command adapters:"
|
|
114
|
+
gen_adapters() {
|
|
115
|
+
mkdir -p "$DEST/.claude/commands" "$DEST/.cursor/commands" "$DEST/.cursor/rules"
|
|
116
|
+
for f in "$DEST"/commands.src/*.md; do
|
|
117
|
+
[ -f "$f" ] || continue
|
|
118
|
+
n=$(basename "$f")
|
|
119
|
+
act "claude .claude/commands/$n"
|
|
120
|
+
act "cursor .cursor/commands/$n"
|
|
121
|
+
[ "$DRY" = 1 ] && continue
|
|
122
|
+
cp -p "$f" "$DEST/.claude/commands/$n"
|
|
123
|
+
awk 'BEGIN{fm=0} NR==1 && $0=="---"{fm=1; next} fm==1 && $0=="---"{fm=0; next} fm==0{print}' "$f" \
|
|
124
|
+
| sed '/^$/{1d}' > "$DEST/.cursor/commands/$n"
|
|
125
|
+
done
|
|
126
|
+
}
|
|
127
|
+
gen_adapters
|
|
128
|
+
|
|
129
|
+
# ---- hybrid JSON merge: ensure the 3 hook entries, never drop user keys
|
|
130
|
+
say ""; say "[3/6] wire hooks (idempotent merge):"
|
|
131
|
+
merge_settings() {
|
|
132
|
+
[ "$DRY" = 1 ] && { act "merge .claude/settings.json + .cursor/hooks.json"; return 0; }
|
|
133
|
+
backup_one "$DEST/.claude/settings.json"; backup_one "$DEST/.cursor/hooks.json"
|
|
134
|
+
python3 - "$DEST" <<'PY'
|
|
135
|
+
import json,os,sys
|
|
136
|
+
dest=sys.argv[1]
|
|
137
|
+
def load(p,default):
|
|
138
|
+
try: return json.load(open(p))
|
|
139
|
+
except Exception: return default
|
|
140
|
+
# Claude settings.json
|
|
141
|
+
sp=os.path.join(dest,".claude/settings.json")
|
|
142
|
+
s=load(sp,{})
|
|
143
|
+
s.setdefault("$schema","https://json.schemastore.org/claude-code-settings.json")
|
|
144
|
+
hooks=s.setdefault("hooks",{})
|
|
145
|
+
def ensure(event,matcher,cmd):
|
|
146
|
+
arr=hooks.setdefault(event,[])
|
|
147
|
+
for blk in arr:
|
|
148
|
+
for h in blk.get("hooks",[]):
|
|
149
|
+
if h.get("command")==cmd: return
|
|
150
|
+
blk={"hooks":[{"type":"command","command":cmd}]}
|
|
151
|
+
if matcher: blk={"matcher":matcher,**blk}
|
|
152
|
+
arr.append(blk)
|
|
153
|
+
ensure("SessionStart","startup|clear|compact","bash .claude/hooks/session-start.sh")
|
|
154
|
+
ensure("PreCompact","auto|manual","bash .claude/hooks/pre-compact.sh")
|
|
155
|
+
ensure("SubagentStop","","bash .claude/hooks/subagent-stop.sh")
|
|
156
|
+
os.makedirs(os.path.dirname(sp),exist_ok=True)
|
|
157
|
+
json.dump(s,open(sp,"w"),indent=2); open(sp,"a").write("\n")
|
|
158
|
+
# Cursor hooks.json
|
|
159
|
+
cp=os.path.join(dest,".cursor/hooks.json")
|
|
160
|
+
c=load(cp,{"version":1,"hooks":{}})
|
|
161
|
+
ch=c.setdefault("hooks",{})
|
|
162
|
+
def cursor_ensure(event,cmd):
|
|
163
|
+
arr=ch.setdefault(event,[])
|
|
164
|
+
if not any(h.get("command")==cmd for h in arr): arr.append({"command":cmd})
|
|
165
|
+
cursor_ensure("sessionStart","bash .claude/hooks/cursor-session-start.sh")
|
|
166
|
+
cursor_ensure("preCompact","bash .claude/hooks/pre-compact.sh")
|
|
167
|
+
cursor_ensure("subagentStop","bash .claude/hooks/subagent-stop.sh")
|
|
168
|
+
os.makedirs(os.path.dirname(cp),exist_ok=True)
|
|
169
|
+
json.dump(c,open(cp,"w"),indent=2); open(cp,"a").write("\n")
|
|
170
|
+
print(" merged .claude/settings.json + .cursor/hooks.json")
|
|
171
|
+
PY
|
|
172
|
+
}
|
|
173
|
+
merge_settings
|
|
174
|
+
|
|
175
|
+
# ---- AGENTS.md / CLAUDE.md non-clobber + VERIFY (bare import + on-disk + SENTINEL)
|
|
176
|
+
say ""; say "[4/6] AGENTS.md / CLAUDE.md verify:"
|
|
177
|
+
# non-clobber: if a *pre-existing user* file differs from kit and we're fresh, keep user copy as-is and stage kit as *.anpunkit.md
|
|
178
|
+
# (in clone/scaffold flow the kit files are the ones on disk; this guards a user who already had their own)
|
|
179
|
+
verify_import() {
|
|
180
|
+
local CL="$DEST/CLAUDE.md" AG="$DEST/AGENTS.md"
|
|
181
|
+
[ -f "$AG" ] || { echo "FATAL VERIFY: AGENTS.md not on disk"; exit 1; }
|
|
182
|
+
# bare top-level @AGENTS.md import (not indented, not fenced)
|
|
183
|
+
if ! grep -qE '^@AGENTS\.md[[:space:]]*$' "$CL"; then
|
|
184
|
+
echo "FATAL VERIFY: CLAUDE.md is missing a bare top-level '@AGENTS.md' import line"; exit 1; fi
|
|
185
|
+
# sentinel present in AGENTS.md
|
|
186
|
+
if ! grep -q 'ANPUNKIT-AGENTS-SENTINEL' "$AG"; then
|
|
187
|
+
echo "FATAL VERIFY: AGENTS.md sentinel missing (file clobbered or empty?)"; exit 1; fi
|
|
188
|
+
say " VERIFY ok: bare @AGENTS.md import + AGENTS.md on disk + sentinel present."
|
|
189
|
+
}
|
|
190
|
+
verify_import
|
|
191
|
+
|
|
192
|
+
# ---- .gitignore: patch, not replace
|
|
193
|
+
say ""; say "[5/6] .gitignore patch:"
|
|
194
|
+
patch_gitignore() {
|
|
195
|
+
local gi="$DEST/.gitignore"
|
|
196
|
+
local -a needles=("docs/.snapshots/" "docs/.kb-snapshot.md" ".anpunkit-backup-*/" "*.anpunkit-new")
|
|
197
|
+
[ "$DRY" = 1 ] && { act "patch .gitignore (${#needles[@]} entries)"; return 0; }
|
|
198
|
+
touch "$gi"
|
|
199
|
+
for n in "${needles[@]}"; do grep -qxF "$n" "$gi" || printf '%s\n' "$n" >> "$gi"; done
|
|
200
|
+
say " patched .gitignore (idempotent)."
|
|
201
|
+
}
|
|
202
|
+
patch_gitignore
|
|
203
|
+
|
|
204
|
+
# ---- KB config (optional)
|
|
205
|
+
say ""; say "[6/6] shared KB:"
|
|
206
|
+
configure_kb() {
|
|
207
|
+
local cfg="$DEST/.claude/kb-config.json"
|
|
208
|
+
if [ "$NO_KB" = 1 ]; then say " --no-kb: skipping KB."; return 0; fi
|
|
209
|
+
if [ -z "$KB_PATH" ] && [ -z "$KB_REMOTE" ]; then
|
|
210
|
+
if [ -t 0 ]; then
|
|
211
|
+
printf " Local path to cloned anpunkit-kb repo (blank to skip): "; read -r KB_PATH || true
|
|
212
|
+
[ -z "$KB_PATH" ] && { say " skipped KB (no path given)."; return 0; }
|
|
213
|
+
else
|
|
214
|
+
say " no KB flags + no TTY -> skipping KB (re-run with --kb-path / --kb-remote)."; return 0
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
if [ -n "$KB_PATH" ] && [ ! -d "$KB_PATH" ]; then
|
|
218
|
+
echo " ! KB path '$KB_PATH' not a directory. Clone your anpunkit-kb repo first."; return 0; fi
|
|
219
|
+
if [ -n "$KB_REMOTE" ]; then
|
|
220
|
+
git ls-remote "$KB_REMOTE" >/dev/null 2>&1 || say " ! warning: cannot reach KB remote '$KB_REMOTE' (configuring anyway)."; fi
|
|
221
|
+
[ "$DRY" = 1 ] && { act "write .claude/kb-config.json"; return 0; }
|
|
222
|
+
mkdir -p "$DEST/.claude"
|
|
223
|
+
python3 -c "import json;json.dump({'path':'$KB_PATH','remote':'$KB_REMOTE'},open('$cfg','w'),indent=2)"
|
|
224
|
+
say " wrote .claude/kb-config.json"
|
|
225
|
+
}
|
|
226
|
+
configure_kb
|
|
227
|
+
|
|
228
|
+
# ---- finalize: stamp the installed manifest (copy the new one)
|
|
229
|
+
if [ "$DRY" != 1 ]; then
|
|
230
|
+
cp -p "$MAN_SRC" "$MAN_DEST" 2>/dev/null || true
|
|
231
|
+
[ -d "$BACKUP" ] && say "" && say "backup written: ${BACKUP#$DEST/}"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
say ""; say "anpunkit setup complete ($MODE). Open Claude Code or Cursor — the SessionStart/sessionStart hook fires automatically."
|
|
235
|
+
if [ "$DRY" = 1 ]; then say "(dry-run: no files were written.)"; fi
|
|
236
|
+
exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# tests/regression/ — cross-phase contract corpus
|
|
2
|
+
|
|
3
|
+
Public-contract / ENDPOINTS-surface tests live here. This is the regression
|
|
4
|
+
guard: every phase CLOSE runs the mock corpus (`scripts/regression.sh`); the
|
|
5
|
+
final phase and `/replan` also run the real corpus (`--real`).
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- A regression test must NOT depend on phase-local fixtures.
|
|
9
|
+
- mock vs real is a fixture/env flag (`TEST_MODE`) on the SAME test — never
|
|
10
|
+
duplicated files.
|
|
11
|
+
- Every `docs/ENDPOINTS.md` entry must have >=1 test here, or phase CLOSE fails.
|