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.
Files changed (66) hide show
  1. package/README.md +44 -0
  2. package/bin/cli.js +41 -0
  3. package/package.json +34 -0
  4. package/template/.claude/agents/debugger.md +47 -0
  5. package/template/.claude/agents/e2e-runner.md +63 -0
  6. package/template/.claude/agents/implementer.md +78 -0
  7. package/template/.claude/agents/infra-provisioner.md +159 -0
  8. package/template/.claude/agents/planner.md +78 -0
  9. package/template/.claude/agents/researcher.md +103 -0
  10. package/template/.claude/agents/synthesizer.md +74 -0
  11. package/template/.claude/agents/test-author.md +71 -0
  12. package/template/.claude/anpunkit-manifest.json +256 -0
  13. package/template/.claude/commands/infra.md +62 -0
  14. package/template/.claude/commands/log-decision.md +34 -0
  15. package/template/.claude/commands/log-issue.md +28 -0
  16. package/template/.claude/commands/overview.md +106 -0
  17. package/template/.claude/commands/phase.md +202 -0
  18. package/template/.claude/commands/quick.md +30 -0
  19. package/template/.claude/commands/replan.md +64 -0
  20. package/template/.claude/commands/store-wisdom.md +195 -0
  21. package/template/.claude/commands/synthesize.md +26 -0
  22. package/template/.claude/commands/unstuck.md +40 -0
  23. package/template/.claude/hooks/cursor-session-start.sh +14 -0
  24. package/template/.claude/hooks/pre-compact.sh +25 -0
  25. package/template/.claude/hooks/session-start.sh +135 -0
  26. package/template/.claude/hooks/subagent-stop.sh +11 -0
  27. package/template/.claude/settings.json +16 -0
  28. package/template/.claude/skills/caveman/SKILL.md +39 -0
  29. package/template/.claude/skills/grill-me/SKILL.md +10 -0
  30. package/template/.claude/skills/karpathy-guidelines/SKILL.md +34 -0
  31. package/template/.cursor/commands/infra.md +57 -0
  32. package/template/.cursor/commands/log-decision.md +29 -0
  33. package/template/.cursor/commands/log-issue.md +23 -0
  34. package/template/.cursor/commands/overview.md +102 -0
  35. package/template/.cursor/commands/phase.md +197 -0
  36. package/template/.cursor/commands/quick.md +25 -0
  37. package/template/.cursor/commands/replan.md +59 -0
  38. package/template/.cursor/commands/store-wisdom.md +191 -0
  39. package/template/.cursor/commands/synthesize.md +22 -0
  40. package/template/.cursor/commands/unstuck.md +36 -0
  41. package/template/.cursor/hooks.json +14 -0
  42. package/template/.cursor/rules/anpunkit.md +11 -0
  43. package/template/.gitattributes +12 -0
  44. package/template/AGENTS.md +216 -0
  45. package/template/CLAUDE.md +70 -0
  46. package/template/README.md +283 -0
  47. package/template/commands.src/infra.md +62 -0
  48. package/template/commands.src/log-decision.md +34 -0
  49. package/template/commands.src/log-issue.md +28 -0
  50. package/template/commands.src/overview.md +106 -0
  51. package/template/commands.src/phase.md +202 -0
  52. package/template/commands.src/quick.md +30 -0
  53. package/template/commands.src/replan.md +64 -0
  54. package/template/commands.src/store-wisdom.md +195 -0
  55. package/template/commands.src/synthesize.md +26 -0
  56. package/template/commands.src/unstuck.md +40 -0
  57. package/template/docker-compose.test.yml +34 -0
  58. package/template/docs/DESIGN_LOG.md +578 -0
  59. package/template/e2e/global-setup.ts +57 -0
  60. package/template/playwright.config.ts +28 -0
  61. package/template/scripts/auth-setup.sh +51 -0
  62. package/template/scripts/e2e-stack.sh +65 -0
  63. package/template/scripts/regression.sh +46 -0
  64. package/template/setup.sh +236 -0
  65. package/template/tests/phase-1/README.md +4 -0
  66. 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,4 @@
1
+ # tests/phase-<n>/ — phase-local suites
2
+
3
+ Phase-local tests live here, one directory per phase. Tests that protect a
4
+ public contract across phases belong in `tests/regression/` instead.
@@ -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.