@whitehatd/crag 0.0.1 → 0.2.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 +864 -15
- package/bin/crag.js +7 -0
- package/package.json +18 -4
- package/src/cli.js +102 -0
- package/src/commands/analyze.js +513 -0
- package/src/commands/check.js +55 -0
- package/src/commands/compile.js +104 -0
- package/src/commands/diff.js +289 -0
- package/src/commands/init.js +112 -0
- package/src/commands/upgrade.js +64 -0
- package/src/commands/workspace.js +94 -0
- package/src/compile/agents-md.js +58 -0
- package/src/compile/atomic-write.js +32 -0
- package/src/compile/cline.js +83 -0
- package/src/compile/cody.js +82 -0
- package/src/compile/continue.js +78 -0
- package/src/compile/copilot.js +70 -0
- package/src/compile/cursor-rules.js +66 -0
- package/src/compile/gemini-md.js +58 -0
- package/src/compile/github-actions.js +165 -0
- package/src/compile/husky.js +66 -0
- package/src/compile/pre-commit.js +50 -0
- package/src/compile/windsurf.js +76 -0
- package/src/compile/zed.js +86 -0
- package/src/crag-agent.md +254 -0
- package/src/governance/gate-to-shell.js +28 -0
- package/src/governance/parse.js +182 -0
- package/src/skills/post-start-validation.md +297 -0
- package/src/skills/pre-start-context.md +506 -0
- package/src/update/integrity.js +131 -0
- package/src/update/skill-sync.js +116 -0
- package/src/update/version-check.js +156 -0
- package/src/workspace/detect.js +190 -0
- package/src/workspace/enumerate.js +270 -0
- package/src/workspace/governance.js +119 -0
- package/cli.js +0 -15
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pre-start-context
|
|
3
|
+
version: 0.2.1
|
|
4
|
+
source_hash: b7be8434b99d5b189c904263e783d573c82109218725cc31fbd4fa1bf81538b6
|
|
5
|
+
description: Universal context loader. Discovers any project's stack, architecture, and state at runtime. Reads governance.md for project-specific rules. Works for any language, framework, or deployment target.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /pre-start-context
|
|
9
|
+
|
|
10
|
+
Run this **before starting any task**. It discovers the project, loads cross-session knowledge, and applies your governance rules — all from the filesystem, nothing hardcoded.
|
|
11
|
+
|
|
12
|
+
> **Chain:** pre-start → task → /post-start-validation. Do not skip post-start.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. Execution Policy
|
|
17
|
+
|
|
18
|
+
Read `.claude/governance.md` for project-specific rules. If it exists, its policies override defaults below.
|
|
19
|
+
|
|
20
|
+
### Sandbox Boundaries
|
|
21
|
+
|
|
22
|
+
All tools and subagents MUST operate within these limits:
|
|
23
|
+
|
|
24
|
+
- **Filesystem:** Write/delete ONLY within this repository (`git rev-parse --show-toplevel`). Read access is broader for discovery, but mutations stay in-repo.
|
|
25
|
+
- **Network:** Only access resources explicitly required by the task (package registries, CI APIs, documented endpoints). NEVER pipe remote content to a shell (`curl|bash`, `wget|sh`).
|
|
26
|
+
- **System:** NEVER modify system files, global packages, PATH, system services, or environment outside this process.
|
|
27
|
+
- **Destructive commands — NEVER run:**
|
|
28
|
+
- `rm -rf` on paths above the project root, `/`, `~`, `$HOME`
|
|
29
|
+
- `dd`, `mkfs`, `fdisk`, `parted` (raw disk operations)
|
|
30
|
+
- `shutdown`, `reboot`, `halt`, `init 0/6`
|
|
31
|
+
- `DROP TABLE/DATABASE/SCHEMA` without explicit user confirmation
|
|
32
|
+
- `docker system prune -a`, `kubectl delete namespace`
|
|
33
|
+
- `git push --force` to main/master
|
|
34
|
+
- `chmod -R 777` or recursive permission changes outside repo
|
|
35
|
+
- **Subagents:** Spawned agents inherit these boundaries. Agent definitions should include `## Boundaries` referencing these rules. Subagents MUST NOT escalate their own permissions.
|
|
36
|
+
|
|
37
|
+
> The `sandbox-guard.sh` hook enforces these rules at the system level. Even if instructions are misread, destructive commands are hard-blocked.
|
|
38
|
+
|
|
39
|
+
**Default AUTO-EXECUTE (unless governance overrides):**
|
|
40
|
+
- Reading files, build, compile, test, lint, format, git operations, package management, environment checks
|
|
41
|
+
- Any command operating on files within this repository
|
|
42
|
+
|
|
43
|
+
**Default ASK FIRST:**
|
|
44
|
+
- Destructive infrastructure (rm containers, drop databases, delete volumes)
|
|
45
|
+
- Production deployments
|
|
46
|
+
- Secrets/credentials modification
|
|
47
|
+
- System-level changes outside this repository
|
|
48
|
+
|
|
49
|
+
### Shell Rule
|
|
50
|
+
|
|
51
|
+
Detect OS and shell. Use appropriate syntax (Unix forward slashes if Git Bash on Windows).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 0.05. Skill Currency Check
|
|
56
|
+
|
|
57
|
+
Check installed skill version:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Read .claude/skills/pre-start-context/SKILL.md
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If the file has a `version:` frontmatter field, compare it to the expected version (0.2.0). If outdated:
|
|
64
|
+
- Report: `"Pre-start skill vX.Y.Z is outdated (v0.2.0 available). Run: crag upgrade"`
|
|
65
|
+
- Continue with current version — never block startup.
|
|
66
|
+
|
|
67
|
+
> This check costs one Read call. If skills are current, no action needed.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 0.1. Session Continuity (Warm Start)
|
|
72
|
+
|
|
73
|
+
Check for a previous session state:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Read .claude/.session-state.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the file exists, check:
|
|
80
|
+
- **Timestamp:** Less than 4 hours old?
|
|
81
|
+
- **Branch:** Same as current? (`git branch --show-current`)
|
|
82
|
+
- **Commit:** Same or ancestor of HEAD? (`git merge-base --is-ancestor <cached-commit> HEAD`)
|
|
83
|
+
|
|
84
|
+
**Warm start** (all three pass):
|
|
85
|
+
- Report: `"Warm start — continuing from <N> minutes ago: <task_summary>"`
|
|
86
|
+
- Previous session's open questions and next steps are immediately relevant
|
|
87
|
+
- If discovery cache is also valid (Section 0.3), most discovery is skipped
|
|
88
|
+
- Skip full MemStack loading if session was recent (< 1 hour) — just load new insights
|
|
89
|
+
|
|
90
|
+
**Cold start** (any check fails):
|
|
91
|
+
- Treat as a fresh session. Full discovery.
|
|
92
|
+
- Stale `.claude/.session-state.json` can be ignored
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 0.2. Intent Classification
|
|
97
|
+
|
|
98
|
+
Before running discovery, classify the task scope from the user's message or skill arguments.
|
|
99
|
+
|
|
100
|
+
| Signals in user message | Scope | Discovery to skip |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| component, style, CSS, UI, page, layout, form, React, Vue | **Frontend** | Backend runtimes, backend architecture |
|
|
103
|
+
| API, endpoint, database, migration, model, query, auth, server | **Backend** | Frontend architecture, frontend configs |
|
|
104
|
+
| Docker, deploy, CI, k8s, infra, pipeline, workflow, terraform | **Infra** | Code architecture (keep runtimes for CI) |
|
|
105
|
+
| README, docs, changelog, license, typo | **Docs** | All code + infra discovery |
|
|
106
|
+
| Ambiguous, multiple domains, or no clear signal | **Full** | Nothing — run everything |
|
|
107
|
+
|
|
108
|
+
Set the discovery scope. Sections 1–3 respect it — skip domains outside scope. Section 4 (Governance) and Section 0.6 (Context Loading) always run regardless of scope.
|
|
109
|
+
|
|
110
|
+
> **Override:** If governance.md contains `## Discovery: full`, always do full discovery regardless of intent.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 0.3. Discovery Cache (Fast Path)
|
|
115
|
+
|
|
116
|
+
Check for a cached discovery state:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Read .claude/.discovery-cache.json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If the cache exists:
|
|
123
|
+
|
|
124
|
+
1. Get current state:
|
|
125
|
+
|
|
126
|
+
// turbo
|
|
127
|
+
```
|
|
128
|
+
git rev-parse --short HEAD
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
// turbo
|
|
132
|
+
```
|
|
133
|
+
git branch --show-current
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
2. Compare to cached `commit` and `branch`
|
|
137
|
+
3. Check if timestamp is less than 4 hours old
|
|
138
|
+
|
|
139
|
+
**Decision tree:**
|
|
140
|
+
|
|
141
|
+
- **Same commit + same branch + < 4 hours → FAST PATH**
|
|
142
|
+
Skip Sections 0.5, 1, 2, 3, 5 entirely. Use cached runtimes, architecture, key files.
|
|
143
|
+
Still run: Section 0.6 (context loading — MemStack may have new data), Section 4 (governance — may have changed).
|
|
144
|
+
Report: `"Fast path — cached discovery (N min old, commit XXXXXX). Skipping full scan."`
|
|
145
|
+
|
|
146
|
+
- **Different commit + < 4 hours → INCREMENTAL**
|
|
147
|
+
Run: `git diff --name-only <cached-commit>..HEAD`
|
|
148
|
+
Only re-discover domains where files changed:
|
|
149
|
+
- `package.json`, `tsconfig.json`, `.js/.ts` files → re-run Node/frontend discovery
|
|
150
|
+
- `Cargo.toml`, `.rs` files → re-run Rust discovery
|
|
151
|
+
- `pyproject.toml`, `.py` files → re-run Python discovery
|
|
152
|
+
- `go.mod`, `.go` files → re-run Go discovery
|
|
153
|
+
- `build.gradle.kts`, `.java/.kt` files → re-run Java discovery
|
|
154
|
+
- `Dockerfile`, `docker-compose*` → re-run infra discovery
|
|
155
|
+
- `governance.md` → always re-read (Section 4)
|
|
156
|
+
Keep cached data for unchanged domains.
|
|
157
|
+
Report: `"Incremental discovery — N files changed since cached session."`
|
|
158
|
+
|
|
159
|
+
- **No cache OR > 4 hours OR different branch → FULL DISCOVERY**
|
|
160
|
+
Run Sections 0.5 through 5 as normal (current behavior).
|
|
161
|
+
Report: `"Full discovery — no valid cache."`
|
|
162
|
+
|
|
163
|
+
**Also at session start:** Delete `.claude/.gates-passed` if it exists — gates must be re-verified each session.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
rm -f .claude/.gates-passed 2>/dev/null
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> The fast path reduces pre-start from 15+ tool calls to 3-4. The incremental path only re-discovers what changed. Full discovery is the fallback.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 0.5. Stack Health
|
|
174
|
+
|
|
175
|
+
Check what optimization tools are available (non-blocking — skip if missing):
|
|
176
|
+
|
|
177
|
+
// turbo
|
|
178
|
+
```
|
|
179
|
+
headroom --version 2>/dev/null && echo "Headroom: OK" || echo "Headroom: not installed"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
// turbo
|
|
183
|
+
```
|
|
184
|
+
rtk --version 2>/dev/null && echo "RTK: OK" || echo "RTK: not installed"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
// turbo
|
|
188
|
+
```
|
|
189
|
+
curl -s http://localhost:8787/health 2>/dev/null && echo "Headroom proxy: running" || echo "Headroom proxy: not running"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 0.6. Context Loading
|
|
195
|
+
|
|
196
|
+
### Step 0: What changed since last session?
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
git log --oneline -5 2>/dev/null || echo "Not a git repo or no commits"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
git diff --stat HEAD~5 -- . ':!node_modules' ':!.next' ':!build' ':!target' ':!dist' ':!__pycache__' 2>/dev/null | tail -10
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> **Adaptive depth:** If nothing changed, abbreviate S1-S4. If only one module changed, focus there. If major structural changes, do a full deep read.
|
|
207
|
+
|
|
208
|
+
### Step 1: Load cross-session memory (if available)
|
|
209
|
+
|
|
210
|
+
Check for MemStack:
|
|
211
|
+
```
|
|
212
|
+
ls .claude/rules/echo.md 2>/dev/null && echo "MemStack rules: loaded" || echo "MemStack: not configured"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If MemStack rules exist, follow them — they trigger context loading from the SQLite database (get-context, get-sessions, get-insights, stale-insights verification).
|
|
216
|
+
|
|
217
|
+
### Step 2: Check CI health
|
|
218
|
+
|
|
219
|
+
Detect CI system and check recent runs:
|
|
220
|
+
```
|
|
221
|
+
ls .github/workflows/*.yml 2>/dev/null && echo "CI: GitHub Actions" || true
|
|
222
|
+
ls .gitlab-ci.yml 2>/dev/null && echo "CI: GitLab CI" || true
|
|
223
|
+
ls Jenkinsfile 2>/dev/null && echo "CI: Jenkins" || true
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
gh run list --limit 3 2>/dev/null || echo "gh CLI not available or not authenticated"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Step 3: Periodic audits (if due)
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
bash -c 'LAST=$(stat -c %Y .claude/.last-audit 2>/dev/null || echo 0); NOW=$(date +%s); DAYS=$(( (NOW - LAST) / 86400 )); if [ "$DAYS" -ge 7 ]; then echo "AUDIT DUE: $DAYS days"; else echo "Audit current ($DAYS days ago)"; fi'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
> If due, spawn skill-auditor and dependency-scanner as background agents after pre-start.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
> **Tool preference:** Use **Read** instead of `cat`, **Glob** instead of `ls`, **Grep** instead of `grep`. Built-in tools are more token-efficient and enable parallel execution.
|
|
241
|
+
|
|
242
|
+
## 1. Environment Discovery
|
|
243
|
+
|
|
244
|
+
Detect the runtime:
|
|
245
|
+
|
|
246
|
+
// turbo
|
|
247
|
+
```
|
|
248
|
+
node --version 2>/dev/null
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
// turbo
|
|
252
|
+
```
|
|
253
|
+
java --version 2>&1 | head -1 2>/dev/null
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
// turbo
|
|
257
|
+
```
|
|
258
|
+
python3 --version 2>/dev/null || python --version 2>/dev/null
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
// turbo
|
|
262
|
+
```
|
|
263
|
+
go version 2>/dev/null
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
// turbo
|
|
267
|
+
```
|
|
268
|
+
rustc --version 2>/dev/null
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
// turbo
|
|
272
|
+
```
|
|
273
|
+
git --version
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
// turbo
|
|
277
|
+
```
|
|
278
|
+
docker --version 2>/dev/null
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
> Only relevant runtimes will return output. Note what's available.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 1.5. Workspace Detection
|
|
286
|
+
|
|
287
|
+
Check if this project is part of a larger workspace:
|
|
288
|
+
|
|
289
|
+
### Workspace markers (check in order)
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
ls pnpm-workspace.yaml 2>/dev/null && echo "Workspace: pnpm"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
Read package.json
|
|
297
|
+
```
|
|
298
|
+
> Check for `"workspaces"` field. If present → npm/yarn workspace.
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
ls Cargo.toml 2>/dev/null
|
|
302
|
+
```
|
|
303
|
+
> Check for `[workspace]` section. If present → Cargo workspace.
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
ls go.work 2>/dev/null && echo "Workspace: Go"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
ls settings.gradle.kts settings.gradle 2>/dev/null && echo "Workspace: Gradle"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
ls nx.json turbo.json 2>/dev/null && echo "Workspace: Nx/Turbo"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
ls .gitmodules 2>/dev/null && echo "Workspace: git submodules"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
If a workspace marker is found:
|
|
322
|
+
1. Enumerate member packages/modules
|
|
323
|
+
2. Check each member for `.claude/governance.md`
|
|
324
|
+
3. Note the workspace type and member list in the discovery cache
|
|
325
|
+
|
|
326
|
+
### Multi-level governance
|
|
327
|
+
|
|
328
|
+
If members have their own governance files, load the hierarchy:
|
|
329
|
+
- Root governance gates are mandatory for all members
|
|
330
|
+
- Member governance gates are additive
|
|
331
|
+
- When running gates (in post-start), merge root + member gates
|
|
332
|
+
|
|
333
|
+
### Independent nested repos
|
|
334
|
+
|
|
335
|
+
If no workspace marker found but multiple `.git` directories exist in child directories:
|
|
336
|
+
- Classify as independent-repos workspace
|
|
337
|
+
- Each child with `.git` is a member
|
|
338
|
+
- Report: `"Workspace: independent repos (N members)"`
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 2. Project Identity
|
|
343
|
+
|
|
344
|
+
Detect the project type and read its configuration:
|
|
345
|
+
|
|
346
|
+
// turbo
|
|
347
|
+
```
|
|
348
|
+
Read README.md
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Detect build system and read config:**
|
|
352
|
+
|
|
353
|
+
// turbo
|
|
354
|
+
```
|
|
355
|
+
Read package.json
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
// turbo
|
|
359
|
+
```
|
|
360
|
+
Read build.gradle.kts
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
// turbo
|
|
364
|
+
```
|
|
365
|
+
Read settings.gradle.kts
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
// turbo
|
|
369
|
+
```
|
|
370
|
+
Read Cargo.toml
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
// turbo
|
|
374
|
+
```
|
|
375
|
+
Read pyproject.toml
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
// turbo
|
|
379
|
+
```
|
|
380
|
+
Read go.mod
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> Most of these will return "file not found" — that's fine. The ones that exist tell you the stack. Read `.env.example` or `.env.template` if they exist for environment variable documentation.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 3. Architecture Map
|
|
388
|
+
|
|
389
|
+
Discover how the project is structured:
|
|
390
|
+
|
|
391
|
+
**Frontend (if detected):**
|
|
392
|
+
```
|
|
393
|
+
Read next.config.ts 2>/dev/null || Read next.config.js 2>/dev/null || Read vite.config.ts 2>/dev/null
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
Glob src/app/* 2>/dev/null || Glob src/pages/* 2>/dev/null || Glob app/* 2>/dev/null
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Backend (if detected):**
|
|
401
|
+
```
|
|
402
|
+
Glob src/main/java/**/controller/* 2>/dev/null || Glob src/controllers/* 2>/dev/null || Glob app/api/* 2>/dev/null
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Services (if multi-service):**
|
|
406
|
+
```
|
|
407
|
+
Read docker-compose.yml 2>/dev/null || Read docker-compose.yaml 2>/dev/null
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
Glob infrastructure/k8s/services/*/deployment.yaml 2>/dev/null || Glob k8s/**/deployment.yaml 2>/dev/null
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**CI/CD:**
|
|
415
|
+
```
|
|
416
|
+
Read .github/workflows/*.yml 2>/dev/null
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
> Count what you find. Note the patterns. Don't hardcode anything — next session will re-discover.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 4. Governance
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
Read .claude/governance.md
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
> This file contains YOUR rules — quality bar, security requirements, gate commands, branch strategy, conventions. Apply everything in it to this session. If it doesn't exist, use sensible defaults.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 5. Key Files
|
|
434
|
+
|
|
435
|
+
Discover critical files by pattern, not by hardcoded path:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
Glob **/application.yml **/application.yaml **/application.properties 2>/dev/null
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
Glob **/.env.example **/.env.template 2>/dev/null
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
Glob **/Dockerfile **/docker-compose*.yml 2>/dev/null
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
Glob **/*.config.ts **/*.config.js **/tsconfig.json 2>/dev/null
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
> Build a mental map of what exists. This is your reference for the rest of the session.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 6. Write Discovery Cache
|
|
458
|
+
|
|
459
|
+
After completing discovery (full or incremental), write the cache for next session.
|
|
460
|
+
|
|
461
|
+
Get the current commit and branch:
|
|
462
|
+
|
|
463
|
+
// turbo
|
|
464
|
+
```
|
|
465
|
+
git rev-parse --short HEAD
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
// turbo
|
|
469
|
+
```
|
|
470
|
+
git branch --show-current
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Write `.claude/.discovery-cache.json` containing:
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{
|
|
477
|
+
"version": 1,
|
|
478
|
+
"timestamp": "<ISO 8601 — use: date -u +%Y-%m-%dT%H:%M:%SZ>",
|
|
479
|
+
"branch": "<current branch>",
|
|
480
|
+
"commit": "<HEAD short hash>",
|
|
481
|
+
"runtimes": {
|
|
482
|
+
"node": "<version or null>",
|
|
483
|
+
"java": "<version or null>",
|
|
484
|
+
"python": "<version or null>",
|
|
485
|
+
"go": "<version or null>",
|
|
486
|
+
"rust": "<version or null>",
|
|
487
|
+
"git": "<version>",
|
|
488
|
+
"docker": "<version or null>"
|
|
489
|
+
},
|
|
490
|
+
"stack_health": {
|
|
491
|
+
"headroom": "<version or null>",
|
|
492
|
+
"rtk": "<version or null>",
|
|
493
|
+
"headroom_proxy": true/false
|
|
494
|
+
},
|
|
495
|
+
"architecture": {
|
|
496
|
+
"type": "<monolith|microservices|cli|library|monorepo>",
|
|
497
|
+
"frontend": "<framework or null>",
|
|
498
|
+
"backend": "<framework or null>",
|
|
499
|
+
"services": ["<service names if multi-service>"],
|
|
500
|
+
"ci": "<github-actions|gitlab-ci|jenkins|none>"
|
|
501
|
+
},
|
|
502
|
+
"key_files": ["<list of discovered config/build files that exist>"]
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
> Cost: one Write call. Savings: 10-15 tool calls skipped on next fast path. The cache is purely advisory — if it's wrong or missing, full discovery runs as normal.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize content for hashing: convert CRLF to LF so Windows and Unix
|
|
8
|
+
* produce identical hashes for semantically identical content.
|
|
9
|
+
*/
|
|
10
|
+
function normalizeForHash(content) {
|
|
11
|
+
return String(content).replace(/\r\n/g, '\n');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compute SHA-256 hash of content after CRLF normalization.
|
|
16
|
+
*/
|
|
17
|
+
function computeHash(content) {
|
|
18
|
+
return crypto.createHash('sha256').update(normalizeForHash(content), 'utf-8').digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse YAML frontmatter from a markdown file.
|
|
23
|
+
* Returns { version, source_hash, name, description, body }
|
|
24
|
+
*/
|
|
25
|
+
function readFrontmatter(filePath) {
|
|
26
|
+
if (!fs.existsSync(filePath)) return null;
|
|
27
|
+
|
|
28
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
30
|
+
if (!match) return { version: null, source_hash: null, name: null, description: null, body: content };
|
|
31
|
+
|
|
32
|
+
const frontmatter = match[1];
|
|
33
|
+
const body = match[2];
|
|
34
|
+
|
|
35
|
+
const get = (key) => {
|
|
36
|
+
const m = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
37
|
+
return m ? m[1].trim() : null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
version: get('version'),
|
|
42
|
+
source_hash: get('source_hash'),
|
|
43
|
+
name: get('name'),
|
|
44
|
+
description: get('description'),
|
|
45
|
+
body,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format a value for YAML frontmatter.
|
|
51
|
+
* Quotes strings that contain special characters or could be ambiguous.
|
|
52
|
+
*/
|
|
53
|
+
function yamlScalar(value) {
|
|
54
|
+
if (value == null) return '';
|
|
55
|
+
const str = String(value);
|
|
56
|
+
|
|
57
|
+
// If contains newline, use literal block scalar
|
|
58
|
+
if (/\r?\n/.test(str)) {
|
|
59
|
+
const indented = str.split(/\r?\n/).map(l => ' ' + l).join('\n');
|
|
60
|
+
return `|\n${indented}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Characters that require quoting in YAML plain scalar:
|
|
64
|
+
// : leading/trailing or followed by space (key separator)
|
|
65
|
+
// # comment marker
|
|
66
|
+
// special markers: & * ! | > ' " % @ `
|
|
67
|
+
// leading/trailing whitespace
|
|
68
|
+
// strings that could be misread as other types: true, false, null, yes, no, numbers
|
|
69
|
+
const needsQuoting =
|
|
70
|
+
/^[\s]|[\s]$/.test(str) ||
|
|
71
|
+
/[:#&*!|>'"%@`]/.test(str) ||
|
|
72
|
+
/^(true|false|null|yes|no|~)$/i.test(str) ||
|
|
73
|
+
/^-?\d+(\.\d+)?$/.test(str) ||
|
|
74
|
+
str === '';
|
|
75
|
+
|
|
76
|
+
if (!needsQuoting) return str;
|
|
77
|
+
|
|
78
|
+
// Use double quotes and escape \ and "
|
|
79
|
+
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update frontmatter fields in a markdown file without modifying the body.
|
|
84
|
+
* Only updates fields that are provided in the meta object.
|
|
85
|
+
* Values are YAML-escaped for safety.
|
|
86
|
+
*/
|
|
87
|
+
function writeFrontmatter(filePath, meta) {
|
|
88
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
90
|
+
|
|
91
|
+
if (!match) {
|
|
92
|
+
// No existing frontmatter — create one
|
|
93
|
+
const fields = Object.entries(meta)
|
|
94
|
+
.filter(([, v]) => v != null)
|
|
95
|
+
.map(([k, v]) => `${k}: ${yamlScalar(v)}`)
|
|
96
|
+
.join('\n');
|
|
97
|
+
fs.writeFileSync(filePath, `---\n${fields}\n---\n${content}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let frontmatter = match[1];
|
|
102
|
+
const body = match[2];
|
|
103
|
+
|
|
104
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
105
|
+
if (value == null) continue;
|
|
106
|
+
const formatted = `${key}: ${yamlScalar(value)}`;
|
|
107
|
+
const regex = new RegExp(`^${key}:.*$`, 'm');
|
|
108
|
+
if (regex.test(frontmatter)) {
|
|
109
|
+
frontmatter = frontmatter.replace(regex, formatted);
|
|
110
|
+
} else {
|
|
111
|
+
frontmatter += `\n${formatted}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(filePath, `---\n${frontmatter}\n---\n${body}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if installed skill body has been modified from its source hash.
|
|
120
|
+
* Returns true if a hash exists and does not match, or if no hash is present
|
|
121
|
+
* (conservative default — "can't verify" is treated as "might be modified").
|
|
122
|
+
*/
|
|
123
|
+
function isModified(filePath) {
|
|
124
|
+
const parsed = readFrontmatter(filePath);
|
|
125
|
+
if (!parsed) return false; // File doesn't exist — nothing to modify
|
|
126
|
+
if (!parsed.source_hash) return true; // No hash present — assume modified (safe default)
|
|
127
|
+
const currentHash = computeHash(parsed.body);
|
|
128
|
+
return currentHash !== parsed.source_hash;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { computeHash, normalizeForHash, readFrontmatter, writeFrontmatter, isModified, yamlScalar };
|