create-anpunkit 2.2.0 → 2.3.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/bin/cli.js +2 -2
- package/package.json +2 -3
- package/template/.claude/agents/debugger.md +1 -1
- package/template/.claude/agents/e2e-runner.md +13 -3
- package/template/.claude/agents/implementer.md +1 -1
- package/template/.claude/agents/infra-provisioner.md +53 -59
- package/template/.claude/agents/planner.md +35 -29
- package/template/.claude/agents/researcher.md +16 -1
- package/template/.claude/agents/spec-author.md +9 -1
- package/template/.claude/agents/synthesizer.md +1 -1
- package/template/.claude/agents/test-author.md +9 -1
- package/template/.claude/anpunkit-manifest.json +52 -113
- package/template/.claude/commands/infra.md +7 -3
- package/template/.claude/commands/log-decision.md +1 -1
- package/template/.claude/commands/log-issue.md +1 -1
- package/template/.claude/commands/overview.md +37 -12
- package/template/.claude/commands/phase.md +50 -25
- package/template/.claude/commands/quick.md +1 -1
- package/template/.claude/commands/replan.md +1 -1
- package/template/.claude/commands/store-wisdom.md +1 -1
- package/template/.claude/commands/synthesize.md +1 -1
- package/template/.claude/commands/unstuck.md +1 -1
- package/template/.claude/hooks/session-start.sh +5 -11
- package/template/.claude/ref/compression.md +56 -0
- package/template/.claude/skills/karpathy-guidelines/SKILL.md +1 -1
- package/template/AGENTS.md +115 -84
- package/template/CLAUDE.md +4 -9
- package/template/README.md +91 -122
- package/template/commands.src/infra.md +7 -3
- package/template/commands.src/log-decision.md +1 -1
- package/template/commands.src/log-issue.md +1 -1
- package/template/commands.src/overview.md +37 -12
- package/template/commands.src/phase.md +50 -25
- package/template/commands.src/quick.md +1 -1
- package/template/commands.src/replan.md +1 -1
- package/template/commands.src/store-wisdom.md +1 -1
- package/template/commands.src/synthesize.md +1 -1
- package/template/commands.src/unstuck.md +1 -1
- package/template/docs/DESIGN_LOG.md +222 -8
- package/template/knowledge/azure.md +161 -0
- package/template/knowledge/webapp.md +265 -0
- package/template/setup.sh +30 -111
- package/template/.claude/hooks/cursor-session-start.sh +0 -17
- package/template/.claude/skills/caveman/SKILL.md +0 -39
- package/template/.cursor/commands/infra.md +0 -82
- package/template/.cursor/commands/log-decision.md +0 -29
- package/template/.cursor/commands/log-issue.md +0 -23
- package/template/.cursor/commands/overview.md +0 -162
- package/template/.cursor/commands/phase.md +0 -296
- package/template/.cursor/commands/quick.md +0 -25
- package/template/.cursor/commands/replan.md +0 -73
- package/template/.cursor/commands/store-wisdom.md +0 -191
- package/template/.cursor/commands/synthesize.md +0 -22
- package/template/.cursor/commands/unstuck.md +0 -36
- package/template/.cursor/hooks.json +0 -14
- package/template/.cursor/rules/anpunkit.md +0 -11
- package/template/anpunkit.png +0 -0
- package/template/docker-compose.test.yml +0 -34
- package/template/e2e/global-setup.ts +0 -57
- package/template/index.html +0 -339
- package/template/playwright.config.ts +0 -28
- package/template/scripts/auth-setup.sh +0 -51
- package/template/scripts/e2e-stack.sh +0 -65
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# knowledge/webapp.md — matured web-app practice (preinstalled)
|
|
2
|
+
|
|
3
|
+
> **What this file is.** Use-case knowledge for browser-facing web apps,
|
|
4
|
+
> consulted by `researcher` (STEP 0.5) when `knowledge/webapp.md` appears in
|
|
5
|
+
> the `knowledge_docs:` list of `docs/OVERVIEW.md`. Selection is deterministic
|
|
6
|
+
> — declared at `/overview`, never inferred (hard rule 10).
|
|
7
|
+
>
|
|
8
|
+
> **Templates below are materialized per-project** — copied to the stated path
|
|
9
|
+
> the first time the phase work needs them. Once materialized they are
|
|
10
|
+
> user-owned project files: adjust freely, the kit never overwrites them.
|
|
11
|
+
|
|
12
|
+
## Browser boundary practice (E2E_KIND: browser)
|
|
13
|
+
|
|
14
|
+
- `ui` spec cases carry a `fixtures/<case-id>-ui.json` descriptor with a
|
|
15
|
+
closed, kit-versioned assert vocabulary: `visible`, `text-equals`,
|
|
16
|
+
`enabled`, `count`. `e2e-runner` EMITS Playwright specs from descriptors —
|
|
17
|
+
it never authors blind from prose.
|
|
18
|
+
- Each emitted spec block carries a `// spec: <case-id>` citation
|
|
19
|
+
(`spec-conformance.sh` checks it).
|
|
20
|
+
- EVIDENCE (hard rule 13): at each UI-existence assertion, capture a
|
|
21
|
+
screenshot regardless of pass/fail to
|
|
22
|
+
`docs/evidence/e2e-phase-<n>/<case-id>-<element-slug>.png` — one shot per
|
|
23
|
+
asserted element, on green as well as red.
|
|
24
|
+
- `ui` cases have NO mock mirror — the browser IS the boundary.
|
|
25
|
+
|
|
26
|
+
## E2E stack ritual
|
|
27
|
+
|
|
28
|
+
Two target modes, recorded in `docs/INFRA.md`:
|
|
29
|
+
|
|
30
|
+
- **azure-deployed** (`E2E_STACK_EXTERNAL=1` in `.env.test`): tests hit the
|
|
31
|
+
deployed app; `e2e-stack.sh up` only health-checks reachability.
|
|
32
|
+
- **local-docker**: `e2e-stack.sh up` builds + starts the compose stack and
|
|
33
|
+
polls health.
|
|
34
|
+
|
|
35
|
+
Sequence, always: `bash scripts/e2e-stack.sh up` -> `npx playwright test` ->
|
|
36
|
+
`bash scripts/e2e-stack.sh down`. Stack didn't come up -> classify
|
|
37
|
+
**STACK NOT READY** (not a bug; no debug budget).
|
|
38
|
+
|
|
39
|
+
## Web deploy ritual (DEPLOY_KIND: cloud-deploy)
|
|
40
|
+
|
|
41
|
+
- Deploy task lives in the final phase (DEPLOY_KIND completion, hard rules).
|
|
42
|
+
- After deploy: record the deployed base URL in `docs/INFRA.md`
|
|
43
|
+
(`## Deployed base URL`) and confirm `docs/ENDPOINTS.md` entries resolve
|
|
44
|
+
against it; run a `/health` (or equivalent) check before calling it done.
|
|
45
|
+
|
|
46
|
+
## .gitignore lines a materialized stack needs
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
e2e/.auth/
|
|
50
|
+
playwright-report/
|
|
51
|
+
test-results/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Templates
|
|
57
|
+
|
|
58
|
+
### `playwright.config.ts` — materialize to project root
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// playwright.config.ts
|
|
62
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
63
|
+
|
|
64
|
+
export default defineConfig({
|
|
65
|
+
testDir: './e2e',
|
|
66
|
+
globalSetup: './e2e/global-setup.ts',
|
|
67
|
+
fullyParallel: false,
|
|
68
|
+
forbidOnly: !!process.env.CI,
|
|
69
|
+
retries: process.env.CI ? 1 : 0,
|
|
70
|
+
workers: 1,
|
|
71
|
+
reporter: [['list'], ['html', { open: 'never' }]],
|
|
72
|
+
use: {
|
|
73
|
+
baseURL: process.env.E2E_BASE_URL || 'http://localhost:8080',
|
|
74
|
+
storageState: 'e2e/.auth/state.json',
|
|
75
|
+
trace: 'retain-on-failure',
|
|
76
|
+
screenshot: 'only-on-failure',
|
|
77
|
+
actionTimeout: 15_000,
|
|
78
|
+
},
|
|
79
|
+
projects: [
|
|
80
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
81
|
+
],
|
|
82
|
+
webServer: process.env.E2E_STACK_EXTERNAL ? undefined : {
|
|
83
|
+
command: 'bash scripts/e2e-stack.sh up',
|
|
84
|
+
url: process.env.E2E_BASE_URL || 'http://localhost:8080',
|
|
85
|
+
timeout: 180_000,
|
|
86
|
+
reuseExistingServer: !process.env.CI,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `e2e/global-setup.ts` — materialize to `e2e/`
|
|
92
|
+
|
|
93
|
+
Entra ID ROPC token injection (Azure/MSAL apps — see `knowledge/azure.md` for
|
|
94
|
+
the auth practice). Known gap: the localStorage keys (`e2e.access_token`,
|
|
95
|
+
`e2e.id_token`) are a stub convention — align them with your app's real MSAL
|
|
96
|
+
cache keys when materializing.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// e2e/global-setup.ts
|
|
100
|
+
import { chromium } from '@playwright/test';
|
|
101
|
+
import * as fs from 'fs';
|
|
102
|
+
import * as path from 'path';
|
|
103
|
+
|
|
104
|
+
const AUTH_DIR = path.join(__dirname, '.auth');
|
|
105
|
+
const STATE = path.join(AUTH_DIR, 'state.json');
|
|
106
|
+
|
|
107
|
+
async function fetchRopcToken() {
|
|
108
|
+
const tenant = reqEnv('E2E_TENANT_ID');
|
|
109
|
+
const body = new URLSearchParams({
|
|
110
|
+
grant_type: 'password',
|
|
111
|
+
client_id: reqEnv('E2E_CLIENT_ID'),
|
|
112
|
+
username: reqEnv('E2E_TEST_USER'),
|
|
113
|
+
password: reqEnv('E2E_TEST_PASSWORD'),
|
|
114
|
+
scope: `openid profile ${reqEnv('E2E_SCOPE')}`,
|
|
115
|
+
});
|
|
116
|
+
const url = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
|
|
117
|
+
const res = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
120
|
+
body,
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
const txt = await res.text();
|
|
124
|
+
throw new Error(
|
|
125
|
+
`ROPC token fetch failed (${res.status}). ` +
|
|
126
|
+
`Check: ROPC enabled on app registration, test user exists, ` +
|
|
127
|
+
`MFA excluded via Conditional Access. Response: ${txt}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return res.json() as Promise<{ access_token: string; id_token: string; expires_in: number }>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reqEnv(k: string): string {
|
|
134
|
+
const v = process.env[k];
|
|
135
|
+
if (!v) throw new Error(`Missing env ${k} — run /infra to regenerate .env.test`);
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function globalSetup() {
|
|
140
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
141
|
+
const token = await fetchRopcToken();
|
|
142
|
+
const baseURL = reqEnv('E2E_BASE_URL');
|
|
143
|
+
const browser = await chromium.launch();
|
|
144
|
+
const page = await browser.newPage();
|
|
145
|
+
await page.goto(baseURL);
|
|
146
|
+
await page.evaluate((t) => {
|
|
147
|
+
localStorage.setItem('e2e.access_token', t.access_token);
|
|
148
|
+
localStorage.setItem('e2e.id_token', t.id_token);
|
|
149
|
+
}, token);
|
|
150
|
+
await page.context().storageState({ path: STATE });
|
|
151
|
+
await browser.close();
|
|
152
|
+
console.log('E2E auth ready — real Entra token injected, MFA UI bypassed.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default globalSetup;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `docker-compose.test.yml` — materialize to project root
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# docker-compose.test.yml — E2E test stack (local-docker mode).
|
|
162
|
+
# TEMPLATE — adjust build contexts and ports for your project layout.
|
|
163
|
+
services:
|
|
164
|
+
backend:
|
|
165
|
+
build:
|
|
166
|
+
context: ./backend # <-- adjust
|
|
167
|
+
environment:
|
|
168
|
+
SQL_SERVER: ${E2E_SQL_SERVER}
|
|
169
|
+
SQL_DB: ${E2E_SQL_DB}
|
|
170
|
+
SQL_USER: ${E2E_SQL_USER}
|
|
171
|
+
SQL_PASSWORD: ${E2E_SQL_PASSWORD}
|
|
172
|
+
TENANT_ID: ${E2E_TENANT_ID}
|
|
173
|
+
CLIENT_ID: ${E2E_CLIENT_ID}
|
|
174
|
+
STORAGE_CONN: ${E2E_STORAGE_CONN:-}
|
|
175
|
+
ports:
|
|
176
|
+
- "8081:8081"
|
|
177
|
+
healthcheck:
|
|
178
|
+
test: ["CMD", "curl", "-fs", "http://localhost:8081/health"]
|
|
179
|
+
interval: 5s
|
|
180
|
+
timeout: 3s
|
|
181
|
+
retries: 12
|
|
182
|
+
|
|
183
|
+
webapp:
|
|
184
|
+
build:
|
|
185
|
+
context: ./frontend # <-- adjust
|
|
186
|
+
environment:
|
|
187
|
+
API_BASE_URL: http://backend:8081
|
|
188
|
+
TENANT_ID: ${E2E_TENANT_ID}
|
|
189
|
+
CLIENT_ID: ${E2E_CLIENT_ID}
|
|
190
|
+
ports:
|
|
191
|
+
- "8080:80"
|
|
192
|
+
depends_on:
|
|
193
|
+
backend:
|
|
194
|
+
condition: service_healthy
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `scripts/e2e-stack.sh` — materialize to `scripts/`
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
#!/usr/bin/env bash
|
|
201
|
+
# scripts/e2e-stack.sh — E2E test stack lifecycle.
|
|
202
|
+
set -euo pipefail
|
|
203
|
+
cd "$(dirname "$0")/.."
|
|
204
|
+
|
|
205
|
+
CMD="${1:-}"
|
|
206
|
+
COMPOSE="docker-compose.test.yml"
|
|
207
|
+
ENVFILE=".env.test"
|
|
208
|
+
|
|
209
|
+
[ -f "$ENVFILE" ] || { echo "missing $ENVFILE — run /infra to generate it"; exit 1; }
|
|
210
|
+
set -a; . "./$ENVFILE"; set +a
|
|
211
|
+
|
|
212
|
+
if [ "${E2E_STACK_EXTERNAL:-}" = "1" ]; then
|
|
213
|
+
echo "[e2e-stack] E2E_STACK_EXTERNAL=1 — targeting deployed Azure app at ${E2E_BASE_URL:-<unset>}"
|
|
214
|
+
case "$CMD" in
|
|
215
|
+
up)
|
|
216
|
+
echo "[e2e-stack] Verifying Azure app is reachable..."
|
|
217
|
+
for i in $(seq 1 10); do
|
|
218
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}/health" >/dev/null 2>&1 \
|
|
219
|
+
|| curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
220
|
+
echo "[e2e-stack] Azure app reachable."
|
|
221
|
+
exit 0
|
|
222
|
+
fi
|
|
223
|
+
sleep 3
|
|
224
|
+
done
|
|
225
|
+
echo "[e2e-stack] WARNING: Azure app not reachable. Classify as AZURE UNAVAILABLE."
|
|
226
|
+
exit 1
|
|
227
|
+
;;
|
|
228
|
+
down)
|
|
229
|
+
echo "[e2e-stack] External mode — nothing to stop."
|
|
230
|
+
exit 0
|
|
231
|
+
;;
|
|
232
|
+
*)
|
|
233
|
+
echo "usage: e2e-stack.sh up|down"
|
|
234
|
+
exit 1
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if docker compose version >/dev/null 2>&1; then DC="docker compose"; else DC="docker-compose"; fi
|
|
240
|
+
|
|
241
|
+
case "$CMD" in
|
|
242
|
+
up)
|
|
243
|
+
echo "[e2e-stack] building + starting local app containers..."
|
|
244
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" up -d --build
|
|
245
|
+
for i in $(seq 1 30); do
|
|
246
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
247
|
+
echo "[e2e-stack] up."
|
|
248
|
+
exit 0
|
|
249
|
+
fi
|
|
250
|
+
sleep 3
|
|
251
|
+
done
|
|
252
|
+
echo "[e2e-stack] ERROR: app did not become healthy — STACK NOT READY"
|
|
253
|
+
exit 1
|
|
254
|
+
;;
|
|
255
|
+
down)
|
|
256
|
+
echo "[e2e-stack] stopping local containers..."
|
|
257
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" down
|
|
258
|
+
echo "[e2e-stack] down."
|
|
259
|
+
;;
|
|
260
|
+
*)
|
|
261
|
+
echo "usage: e2e-stack.sh up|down"
|
|
262
|
+
exit 1
|
|
263
|
+
;;
|
|
264
|
+
esac
|
|
265
|
+
```
|
package/template/setup.sh
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
# user-owned -> NEVER overwritten
|
|
12
12
|
# hybrid -> merged idempotently, never replaced
|
|
13
13
|
#
|
|
14
|
+
# Claude Code only (v2.3 — Cursor support dropped, §5.69).
|
|
14
15
|
# Flags: --src DIR --kb-path P --kb-remote URL --no-kb --force --dry-run
|
|
15
|
-
# --tools LIST (comma/space list from: claude cursor; default both)
|
|
16
|
-
# --add-tool T (additive: install T's files into an existing project)
|
|
17
16
|
set -euo pipefail
|
|
18
17
|
|
|
19
18
|
SRC="."
|
|
@@ -22,9 +21,6 @@ KB_REMOTE="${ANPUNKIT_KB_REMOTE:-}"
|
|
|
22
21
|
NO_KB=0
|
|
23
22
|
FORCE=0
|
|
24
23
|
DRY=0
|
|
25
|
-
TOOLS_FLAG=""
|
|
26
|
-
ADD_TOOL=""
|
|
27
|
-
AVAILABLE_TOOLS="claude cursor" # tools with real adapters; extend here
|
|
28
24
|
while [ $# -gt 0 ]; do
|
|
29
25
|
case "$1" in
|
|
30
26
|
--src) SRC="$2"; shift 2;;
|
|
@@ -33,8 +29,6 @@ while [ $# -gt 0 ]; do
|
|
|
33
29
|
--no-kb) NO_KB=1; shift;;
|
|
34
30
|
--force) FORCE=1; shift;;
|
|
35
31
|
--dry-run) DRY=1; shift;;
|
|
36
|
-
--tools) TOOLS_FLAG="$2"; shift 2;;
|
|
37
|
-
--add-tool) ADD_TOOL="$2"; shift 2;;
|
|
38
32
|
*) echo "unknown flag: $1" >&2; exit 2;;
|
|
39
33
|
esac
|
|
40
34
|
done
|
|
@@ -70,36 +64,6 @@ fi
|
|
|
70
64
|
say "mode: $MODE (installed: ${OLD_VER:-none} -> new: $NEW_VER)"
|
|
71
65
|
[ "$MODE" = "newer-installed" ] && say " ! installed version is newer than this package — proceeding as repair, review carefully."
|
|
72
66
|
|
|
73
|
-
# ---- tool selection (v2.1): install only the selected tools' adapters/hooks/files
|
|
74
|
-
TOOLS_FILE="$DEST/.claude/anpunkit-tools.json"
|
|
75
|
-
norm_tools() { printf '%s' "$1" | tr ',' ' ' | tr -s ' ' | sed 's/^ //; s/ $//'; }
|
|
76
|
-
read_recorded_tools() {
|
|
77
|
-
[ -f "$TOOLS_FILE" ] || { printf ''; return; }
|
|
78
|
-
node -e "try{console.log((JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).tools||[]).join(' '))}catch(e){console.log('')}" "$TOOLS_FILE" 2>/dev/null || printf ''
|
|
79
|
-
}
|
|
80
|
-
valid_tool() { case " $AVAILABLE_TOOLS " in *" $1 "*) return 0;; *) return 1;; esac; }
|
|
81
|
-
|
|
82
|
-
RECORDED="$(read_recorded_tools)"
|
|
83
|
-
if [ -n "$TOOLS_FLAG" ]; then
|
|
84
|
-
TOOLS="$(norm_tools "$TOOLS_FLAG")"
|
|
85
|
-
elif [ -n "$RECORDED" ]; then
|
|
86
|
-
TOOLS="$RECORDED" # re-run: preserve prior selection
|
|
87
|
-
elif [ "$MODE" = "fresh" ] && [ -t 0 ]; then
|
|
88
|
-
printf " Select tools to install (space-separated from: %s) [claude cursor]: " "$AVAILABLE_TOOLS"
|
|
89
|
-
read -r _sel || true
|
|
90
|
-
TOOLS="$(norm_tools "${_sel:-claude cursor}")"
|
|
91
|
-
else
|
|
92
|
-
TOOLS="claude cursor" # non-interactive default = both (no regression)
|
|
93
|
-
fi
|
|
94
|
-
# additive: --add-tool unions into the selection (and is recorded)
|
|
95
|
-
[ -n "$ADD_TOOL" ] && TOOLS="$(norm_tools "$TOOLS $(norm_tools "$ADD_TOOL")" | tr ' ' '\n' | sort -u | tr '\n' ' ')"
|
|
96
|
-
TOOLS="$(norm_tools "$TOOLS")"
|
|
97
|
-
# validate
|
|
98
|
-
for t in $TOOLS; do valid_tool "$t" || { echo "FATAL: unknown tool '$t' (available: $AVAILABLE_TOOLS)"; exit 2; }; done
|
|
99
|
-
[ -z "$TOOLS" ] && { echo "FATAL: no tools selected"; exit 2; }
|
|
100
|
-
tool_selected() { case " $TOOLS " in *" $1 "*) return 0;; *) return 1;; esac; }
|
|
101
|
-
say "tools: $TOOLS$([ -n "$ADD_TOOL" ] && echo ' (+'"$ADD_TOOL"' added)')"
|
|
102
|
-
|
|
103
67
|
BACKUP="$DEST/.anpunkit-backup-$(date +%Y%m%d-%H%M%S)"
|
|
104
68
|
backup_one() {
|
|
105
69
|
[ "$MODE" = "fresh" ] && return 0
|
|
@@ -114,11 +78,6 @@ backup_one() {
|
|
|
114
78
|
install_kit_file() {
|
|
115
79
|
local rel="$1" s="$SRC/$1" d="$DEST/$1"
|
|
116
80
|
[ -f "$s" ] || return 0
|
|
117
|
-
# v2.1 tool gating: cursor-only files skipped unless cursor selected.
|
|
118
|
-
# (.claude/* is shared infra — Cursor reads .claude/agents + .claude/hooks too.)
|
|
119
|
-
case "$rel" in
|
|
120
|
-
.cursor/*) tool_selected cursor || { act "skip $rel (cursor not selected)"; return 0; };;
|
|
121
|
-
esac
|
|
122
81
|
if [ ! -f "$d" ]; then
|
|
123
82
|
act "write $rel"; [ "$DRY" = 1 ] && return 0
|
|
124
83
|
mkdir -p "$(dirname "$d")"; cp -p "$s" "$d"; return 0
|
|
@@ -156,76 +115,44 @@ else
|
|
|
156
115
|
fi
|
|
157
116
|
|
|
158
117
|
# ---- generate command adapters from commands.src/ (manifest-owned output)
|
|
159
|
-
say ""; say "[2/6] generate command adapters (
|
|
118
|
+
say ""; say "[2/6] generate command adapters (.claude/commands/):"
|
|
160
119
|
gen_adapters() {
|
|
161
|
-
|
|
162
|
-
tool_selected cursor && mkdir -p "$DEST/.cursor/commands" "$DEST/.cursor/rules"
|
|
120
|
+
mkdir -p "$DEST/.claude/commands"
|
|
163
121
|
for f in "$DEST"/commands.src/*.md; do
|
|
164
122
|
[ -f "$f" ] || continue
|
|
165
123
|
n=$(basename "$f")
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
[ "$DRY" = 1 ] || cp -p "$f" "$DEST/.claude/commands/$n"
|
|
169
|
-
fi
|
|
170
|
-
if tool_selected cursor; then
|
|
171
|
-
act "cursor .cursor/commands/$n"
|
|
172
|
-
[ "$DRY" = 1 ] || awk 'BEGIN{fm=0} NR==1 && $0=="---"{fm=1; next} fm==1 && $0=="---"{fm=0; next} fm==0{print}' "$f" \
|
|
173
|
-
| sed '/^$/{1d}' > "$DEST/.cursor/commands/$n"
|
|
174
|
-
fi
|
|
124
|
+
act "claude .claude/commands/$n"
|
|
125
|
+
[ "$DRY" = 1 ] || cp -p "$f" "$DEST/.claude/commands/$n"
|
|
175
126
|
done
|
|
176
127
|
}
|
|
177
128
|
gen_adapters
|
|
178
129
|
|
|
179
|
-
# ---- hybrid JSON merge: ensure the hook entries
|
|
180
|
-
say ""; say "[3/6] wire hooks (idempotent merge,
|
|
130
|
+
# ---- hybrid JSON merge: ensure the Claude hook entries, never drop user keys
|
|
131
|
+
say ""; say "[3/6] wire hooks (idempotent merge, .claude/settings.json):"
|
|
181
132
|
merge_settings() {
|
|
182
|
-
[ "$DRY" = 1 ] && { act "merge
|
|
183
|
-
|
|
184
|
-
tool_selected cursor && backup_one "$DEST/.cursor/hooks.json"
|
|
185
|
-
CLAUDE_SEL=$(tool_selected claude && echo 1 || echo 0) \
|
|
186
|
-
CURSOR_SEL=$(tool_selected cursor && echo 1 || echo 0) \
|
|
133
|
+
[ "$DRY" = 1 ] && { act "merge .claude/settings.json"; return 0; }
|
|
134
|
+
backup_one "$DEST/.claude/settings.json"
|
|
187
135
|
node - "$DEST" <<'NODE'
|
|
188
136
|
const fs=require('fs'),path=require('path');
|
|
189
137
|
const dest=process.argv[2];
|
|
190
|
-
const claudeSel=process.env.CLAUDE_SEL==='1', cursorSel=process.env.CURSOR_SEL==='1';
|
|
191
138
|
const load=(p,d)=>{try{return JSON.parse(fs.readFileSync(p,'utf8'))}catch(e){return d}};
|
|
192
139
|
const save=(p,o)=>{fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(o,null,2)+"\n")};
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
if(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
ensure('SubagentStop','','bash .claude/hooks/subagent-stop.sh');
|
|
210
|
-
save(sp,s);
|
|
211
|
-
out.push('.claude/settings.json');
|
|
212
|
-
}
|
|
213
|
-
// Cursor hooks.json
|
|
214
|
-
if(cursorSel){
|
|
215
|
-
const cp=path.join(dest,'.cursor/hooks.json');
|
|
216
|
-
const c=load(cp,{version:1,hooks:{}});
|
|
217
|
-
c.hooks=c.hooks||{};
|
|
218
|
-
const cursorEnsure=(event,cmd)=>{
|
|
219
|
-
const arr=c.hooks[event]=c.hooks[event]||[];
|
|
220
|
-
if(!arr.some(h=>h.command===cmd))arr.push({command:cmd});
|
|
221
|
-
};
|
|
222
|
-
cursorEnsure('sessionStart','bash .claude/hooks/cursor-session-start.sh');
|
|
223
|
-
cursorEnsure('preCompact','bash .claude/hooks/pre-compact.sh');
|
|
224
|
-
cursorEnsure('subagentStop','bash .claude/hooks/subagent-stop.sh');
|
|
225
|
-
save(cp,c);
|
|
226
|
-
out.push('.cursor/hooks.json');
|
|
227
|
-
}
|
|
228
|
-
console.log(' merged '+(out.join(' + ')||'(nothing — no tool selected hooks)'));
|
|
140
|
+
const sp=path.join(dest,'.claude/settings.json');
|
|
141
|
+
const s=load(sp,{});
|
|
142
|
+
if(!s.$schema)s.$schema='https://json.schemastore.org/claude-code-settings.json';
|
|
143
|
+
s.hooks=s.hooks||{};
|
|
144
|
+
const ensure=(event,matcher,cmd)=>{
|
|
145
|
+
const arr=s.hooks[event]=s.hooks[event]||[];
|
|
146
|
+
for(const blk of arr)for(const h of (blk.hooks||[]))if(h.command===cmd)return;
|
|
147
|
+
const blk={hooks:[{type:'command',command:cmd}]};
|
|
148
|
+
if(matcher)blk.matcher=matcher;
|
|
149
|
+
arr.push(blk);
|
|
150
|
+
};
|
|
151
|
+
ensure('SessionStart','startup|clear|compact','bash .claude/hooks/session-start.sh');
|
|
152
|
+
ensure('PreCompact','auto|manual','bash .claude/hooks/pre-compact.sh');
|
|
153
|
+
ensure('SubagentStop','','bash .claude/hooks/subagent-stop.sh');
|
|
154
|
+
save(sp,s);
|
|
155
|
+
console.log(' merged .claude/settings.json');
|
|
229
156
|
NODE
|
|
230
157
|
}
|
|
231
158
|
merge_settings
|
|
@@ -237,17 +164,11 @@ say ""; say "[4/6] AGENTS.md / CLAUDE.md verify:"
|
|
|
237
164
|
verify_import() {
|
|
238
165
|
local CL="$DEST/CLAUDE.md" AG="$DEST/AGENTS.md"
|
|
239
166
|
[ -f "$AG" ] || { echo "FATAL VERIFY: AGENTS.md not on disk"; exit 1; }
|
|
240
|
-
# sentinel present in AGENTS.md (shared — both tools read AGENTS.md)
|
|
241
167
|
if ! grep -q 'ANPUNKIT-AGENTS-SENTINEL' "$AG"; then
|
|
242
168
|
echo "FATAL VERIFY: AGENTS.md sentinel missing (file clobbered or empty?)"; exit 1; fi
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
echo "FATAL VERIFY: CLAUDE.md is missing a bare top-level '@AGENTS.md' import line"; exit 1; fi
|
|
247
|
-
say " VERIFY ok: bare @AGENTS.md import + AGENTS.md on disk + sentinel present."
|
|
248
|
-
else
|
|
249
|
-
say " VERIFY ok: AGENTS.md on disk + sentinel present (claude not selected — CLAUDE.md import not required)."
|
|
250
|
-
fi
|
|
169
|
+
if ! grep -qE '^@AGENTS\.md[[:space:]]*$' "$CL"; then
|
|
170
|
+
echo "FATAL VERIFY: CLAUDE.md is missing a bare top-level '@AGENTS.md' import line"; exit 1; fi
|
|
171
|
+
say " VERIFY ok: bare @AGENTS.md import + AGENTS.md on disk + sentinel present."
|
|
251
172
|
}
|
|
252
173
|
verify_import
|
|
253
174
|
|
|
@@ -305,14 +226,12 @@ configure_kb() {
|
|
|
305
226
|
|
|
306
227
|
configure_kb
|
|
307
228
|
|
|
308
|
-
# ---- finalize: stamp the installed manifest (copy the new one)
|
|
229
|
+
# ---- finalize: stamp the installed manifest (copy the new one)
|
|
309
230
|
if [ "$DRY" != 1 ]; then
|
|
310
231
|
cp -p "$MAN_SRC" "$MAN_DEST" 2>/dev/null || true
|
|
311
|
-
mkdir -p "$DEST/.claude"
|
|
312
|
-
node -e "require('fs').writeFileSync(process.argv[1],JSON.stringify({tools:process.argv[2].split(' ').filter(Boolean)},null,2)+'\n')" "$TOOLS_FILE" "$TOOLS" 2>/dev/null || true
|
|
313
232
|
[ -d "$BACKUP" ] && say "" && say "backup written: ${BACKUP#$DEST/}"
|
|
314
233
|
fi
|
|
315
234
|
|
|
316
|
-
say ""; say "anpunkit setup complete ($MODE
|
|
235
|
+
say ""; say "anpunkit setup complete ($MODE). Open Claude Code — the SessionStart hook fires automatically."
|
|
317
236
|
if [ "$DRY" = 1 ]; then say "(dry-run: no files were written.)"; fi
|
|
318
237
|
exit 0
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# cursor-session-start.sh — Cursor wiring for the SESSION-OPEN ritual.
|
|
3
|
-
# Cursor's sessionStart hook expects JSON on stdout ({"additional_context": ...}),
|
|
4
|
-
# unlike Claude Code, which injects raw stdout. This wrapper runs the SHARED
|
|
5
|
-
# session-start.sh body (single copy — anti-drift) and wraps its output in the
|
|
6
|
-
# JSON envelope Cursor requires. Cursor sets CLAUDE_PROJECT_DIR (compat alias),
|
|
7
|
-
# so the shared script needs no changes.
|
|
8
|
-
# Uses node for JSON (the kit assumes Node; python3 is a fallback only).
|
|
9
|
-
# Verified against cursor.com/docs/hooks (sessionStart output schema), 2026-06.
|
|
10
|
-
set -euo pipefail
|
|
11
|
-
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
-
OUT="$(bash "$HERE/session-start.sh" 2>/dev/null || true)"
|
|
13
|
-
if command -v node >/dev/null 2>&1; then
|
|
14
|
-
printf '%s' "$OUT" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(JSON.stringify({additional_context:d})))'
|
|
15
|
-
else
|
|
16
|
-
printf '%s' "$OUT" | python3 -c 'import json,sys; print(json.dumps({"additional_context": sys.stdin.read()}))'
|
|
17
|
-
fi
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: caveman
|
|
3
|
-
description: >
|
|
4
|
-
Ultra-compressed communication mode. Cuts token usage ~75% by dropping
|
|
5
|
-
filler, articles, and pleasantries while keeping full technical accuracy.
|
|
6
|
-
Use when user says "caveman mode", "talk like caveman", "use caveman",
|
|
7
|
-
"less tokens", "be brief", or invokes /caveman.
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
11
|
-
|
|
12
|
-
## Persistence
|
|
13
|
-
|
|
14
|
-
ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drift. Still active if unsure. Off only when user says "stop caveman" or "normal mode".
|
|
15
|
-
|
|
16
|
-
## Rules
|
|
17
|
-
|
|
18
|
-
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
|
|
19
|
-
|
|
20
|
-
Technical terms stay exact. Code blocks unchanged. Errors quoted exact.
|
|
21
|
-
|
|
22
|
-
Pattern: `[thing] [action] [reason]. [next step].`
|
|
23
|
-
|
|
24
|
-
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
25
|
-
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
26
|
-
|
|
27
|
-
### Examples
|
|
28
|
-
|
|
29
|
-
**"Why React component re-render?"**
|
|
30
|
-
|
|
31
|
-
> Inline obj prop -> new ref -> re-render. `useMemo`.
|
|
32
|
-
|
|
33
|
-
**"Explain database connection pooling."**
|
|
34
|
-
|
|
35
|
-
> Pool = reuse DB conn. Skip handshake -> fast under load.
|
|
36
|
-
|
|
37
|
-
## Auto-Clarity Exception
|
|
38
|
-
|
|
39
|
-
Drop caveman temporarily for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after.
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
Caveman ULTRA mode. You are the ORCHESTRATOR.
|
|
2
|
-
|
|
3
|
-
Action: $ARGUMENTS (default: "provision" if INFRA.md missing, "verify" if present)
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## PRE-FLIGHT
|
|
8
|
-
|
|
9
|
-
1. Check `az account show` — if it fails, STOP.
|
|
10
|
-
Tell me: "Run `scripts/auth-setup.sh` first."
|
|
11
|
-
|
|
12
|
-
2. Determine mode from $ARGUMENTS or default logic.
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## PROVISION / UPDATE flow
|
|
17
|
-
|
|
18
|
-
1. Dispatch `infra-provisioner` with the mode.
|
|
19
|
-
|
|
20
|
-
2. It returns WHAT-IF READY. Show me:
|
|
21
|
-
- resource list with SKUs and costs
|
|
22
|
-
- total monthly cost estimate
|
|
23
|
-
- what-if detail file path
|
|
24
|
-
Then ask:
|
|
25
|
-
> "Review docs/research/infra-whatif-<timestamp>.md. Type 'go' to apply,
|
|
26
|
-
> or describe changes to make first."
|
|
27
|
-
WAIT. Do not proceed until I say "go".
|
|
28
|
-
|
|
29
|
-
3. On "go": pass APPROVED to `infra-provisioner`.
|
|
30
|
-
|
|
31
|
-
4. On "make changes": dispatch UPDATE with feedback. Loop to step 2.
|
|
32
|
-
|
|
33
|
-
5. On APPLIED: tell me INFRA.md ✓, .env.test ✓, and any manual steps remaining.
|
|
34
|
-
Then run AUTH PROOF (below) before declaring Phase 0 complete.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## AUTH PROOF (one-time, hard rule 16)
|
|
39
|
-
|
|
40
|
-
Runs after a successful PROVISION (and on demand via `/infra auth-proof`). Proves
|
|
41
|
-
every credential the project's real tests will use is reusable WITHOUT interaction.
|
|
42
|
-
"Reusable" is defined falsifiably: obtainable headlessly TWICE IN A ROW.
|
|
43
|
-
|
|
44
|
-
1. Enumerate the credentials in scope: the Entra/MSAL app login, plus every
|
|
45
|
-
external datasource credential referenced in docs/DATAFLOW.md (external rows)
|
|
46
|
-
and docs/ENDPOINTS.md (auth column) — Azure SQL, Tableau, etc.
|
|
47
|
-
|
|
48
|
-
2. Dispatch `infra-provisioner` to run, for EACH credential, a headless obtain
|
|
49
|
-
twice in a row: first call primes (token fetch / connect), second call must
|
|
50
|
-
succeed from cache/refresh with ZERO prompts. The Microsoft login UI is never
|
|
51
|
-
driven (ROPC + dedicated MFA-excluded test account; hard rule 8).
|
|
52
|
-
|
|
53
|
-
3. Result:
|
|
54
|
-
- All credentials pass twice headlessly -> write `AUTH PROOF: PASS <timestamp>`
|
|
55
|
-
to docs/INFRA.md with the per-credential list. Phase 0 may complete.
|
|
56
|
-
- Any credential prompts or fails the second obtain -> NOT reusable. Write
|
|
57
|
-
`AUTH PROOF: FAIL` + which credential, STOP, tell me what to fix. Phase 0 is
|
|
58
|
-
not complete until the proof passes.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## VERIFY flow
|
|
63
|
-
|
|
64
|
-
1. Check INFRA.md exists. If missing: tell me to run `/infra provision` first.
|
|
65
|
-
2. Dispatch `infra-provisioner` VERIFY mode.
|
|
66
|
-
3. Return drift report. If the AUTH PROOF marker is missing or stale, re-run AUTH PROOF.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## REGENERATE-ENV flow
|
|
71
|
-
|
|
72
|
-
1. Check INFRA.md exists.
|
|
73
|
-
2. Dispatch `infra-provisioner` REGENERATE-ENV.
|
|
74
|
-
3. Confirm .env.test rewritten.
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Notes
|
|
79
|
-
|
|
80
|
-
- /infra never touches PLAN.md, STATE.md, ISSUES.md, or DESIGN_LOG.md.
|
|
81
|
-
- Infra errors are AZURE UNAVAILABLE or CONFIG ERROR — do NOT route to debugger.
|
|
82
|
-
- If UNDERSPEC: list missing decisions, ask me to fill them, re-dispatch.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
Caveman ULTRA mode.
|
|
2
|
-
|
|
3
|
-
Append an architectural decision to docs/DESIGN_LOG.md. Change: $ARGUMENTS
|
|
4
|
-
|
|
5
|
-
WHEN this applies — the change is ARCHITECTURAL:
|
|
6
|
-
- new / removed / renamed agent, hook, or command
|
|
7
|
-
- changed workflow rule (escalation, phase gate, test layering)
|
|
8
|
-
- design decision where alternatives were weighed
|
|
9
|
-
|
|
10
|
-
WHEN it does NOT apply:
|
|
11
|
-
- code changes, features, phase progress -> HISTORY.md
|
|
12
|
-
- bugs + fixes -> ISSUES.md
|
|
13
|
-
- infra changes -> INFRA.md
|
|
14
|
-
|
|
15
|
-
Steps:
|
|
16
|
-
1. Read docs/DESIGN_LOG.md.
|
|
17
|
-
2. Append a dated line to `## 0. Changelog`, newest first.
|
|
18
|
-
3. If alternatives were weighed, ALSO append a full entry to §5:
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### 5.x <decision title>
|
|
22
|
-
|
|
23
|
-
Options: <a, b, c>. **Chosen: <x>.** <why it won.>
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
4. If agent/hook/command/file added or removed, update the file inventory (§6).
|
|
27
|
-
5. NEVER rewrite or delete existing entries — append only.
|
|
28
|
-
|
|
29
|
-
Confirm back in 2-3 lines: what was logged and where.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
Caveman ULTRA mode.
|
|
2
|
-
|
|
3
|
-
Append an entry to docs/ISSUES.md. Error: $ARGUMENTS
|
|
4
|
-
|
|
5
|
-
Canonical format:
|
|
6
|
-
```
|
|
7
|
-
|
|
8
|
-
### <short error title — searchable, literal error keywords>
|
|
9
|
-
|
|
10
|
-
- [x] open (or “- [x] resolved”)
|
|
11
|
-
- symptom: <what was observed>
|
|
12
|
-
- root cause: <the REAL underlying cause>
|
|
13
|
-
- solution: <exact fix, or “pending”>
|
|
14
|
-
- failed attempts: <approaches that did NOT work>
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
Rules:
|
|
18
|
-
- title must contain literal error keywords -> grep finds it.
|
|
19
|
-
- root cause is the real cause, not "the line threw an error".
|
|
20
|
-
- always fill "failed attempts" — stops repeated dead ends.
|
|
21
|
-
- open issues go at the TOP; resolved below; archived oldest at bottom.
|
|
22
|
-
|
|
23
|
-
Confirm the entry back in 2 lines.
|