@su-record/vibe 2.9.20 → 2.9.22
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/CLAUDE.md +4 -3
- package/commands/vibe.contract.md +29 -29
- package/commands/vibe.regress.md +20 -20
- package/commands/vibe.run.md +6 -6
- package/commands/vibe.spec.md +6 -6
- package/commands/vibe.test.md +96 -0
- package/commands/vibe.verify.md +9 -9
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +82 -1
- package/hooks/scripts/pre-tool-guard.js +60 -19
- package/package.json +1 -1
- package/skills/vibe-contract/SKILL.md +58 -58
- package/skills/vibe-regress/SKILL.md +94 -94
- package/skills/vibe-spec/SKILL.md +12 -12
- package/skills/vibe-test/SKILL.md +247 -0
package/CLAUDE.md
CHANGED
|
@@ -69,8 +69,9 @@ No `console.log` in commits · No hardcoded strings/numbers · No commented-out
|
|
|
69
69
|
`/vibe.spec` is the single entry point — orchestrates interview → plan → spec → review → `/vibe.run` → `/vibe.verify` → `/vibe.contract` → `/vibe.trace`. For UI types (website/webapp/mobile), `/vibe.figma` branches in parallel. Smart Resume detects existing `.claude/vibe/{interviews,plans,specs}/*.md` to skip phases.
|
|
70
70
|
|
|
71
71
|
**Quality-loop commands** (bug → prevention):
|
|
72
|
-
- `/vibe.regress` —
|
|
73
|
-
- `/vibe.contract` — API
|
|
72
|
+
- `/vibe.regress` — Regression test auto-evolution. Auto-registers on `/vibe.verify` failure; `generate` produces preventive tests; `cluster` promotes recurring patterns.
|
|
73
|
+
- `/vibe.contract` — API contract drift detection. Compares the contract extracted from the SPEC against the implementation; P1 drift auto-propagates to `/vibe.regress`.
|
|
74
|
+
- `/vibe.test` — vibe self-test across the CC ↔ coco harnesses. Subcommands: `parity` (static), `report` (runtime), `compare` (diff). P1 drift auto-propagates to `/vibe.regress`. Recommended before every release.
|
|
74
75
|
|
|
75
76
|
| Task Size | Approach |
|
|
76
77
|
|---|---|
|
|
@@ -98,7 +99,7 @@ No `console.log` in commits · No hardcoded strings/numbers · No commented-out
|
|
|
98
99
|
|
|
99
100
|
## Git
|
|
100
101
|
|
|
101
|
-
**Include**: `.claude/vibe/{plans,specs,features,todos,research,regressions,contracts}/`, `.claude/vibe/config.json`, `CLAUDE.md`
|
|
102
|
+
**Include**: `.claude/vibe/{plans,specs,features,todos,research,regressions,contracts,test-reports}/`, `.claude/vibe/config.json`, `CLAUDE.md`
|
|
102
103
|
**Exclude**: `~/.claude/{rules,commands,agents,skills}/`, `.claude/settings.local.json`
|
|
103
104
|
|
|
104
105
|
<!-- VIBE:END -->
|
|
@@ -5,57 +5,57 @@ argument-hint: "extract | check | diff [feature-name]"
|
|
|
5
5
|
|
|
6
6
|
# /vibe.contract
|
|
7
7
|
|
|
8
|
-
**API Contract Drift Detection** —
|
|
8
|
+
**API Contract Drift Detection** — when implementation diverges from the SPEC's API contract, catch it immediately.
|
|
9
9
|
|
|
10
|
-
> SPEC
|
|
10
|
+
> The SPEC is the source of truth. If the implementation silently leaves the SPEC, tests can pass while the contract breaks.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
/vibe.contract extract <feature> # SPEC
|
|
16
|
-
/vibe.contract check <feature> #
|
|
17
|
-
/vibe.contract diff <feature> #
|
|
15
|
+
/vibe.contract extract <feature> # SPEC → contract record at .claude/vibe/contracts/<feature>.md
|
|
16
|
+
/vibe.contract check <feature> # contract vs implementation, drift report
|
|
17
|
+
/vibe.contract diff <feature> # changed fields since last check
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## What counts as an "API contract"
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
A contract = any **interface shape** that external consumers (clients, other services) depend on:
|
|
23
23
|
|
|
24
24
|
- HTTP endpoint: method + path + request schema + response schema + status codes
|
|
25
25
|
- GraphQL: query/mutation name + args + return shape
|
|
26
|
-
-
|
|
27
|
-
-
|
|
26
|
+
- Event/message: topic + payload schema
|
|
27
|
+
- Exported TypeScript function signature (when explicitly marked as public API)
|
|
28
28
|
|
|
29
29
|
## Process
|
|
30
30
|
|
|
31
31
|
Load skill `vibe-contract` with subcommand: `$ARGUMENTS`
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
**Core steps**:
|
|
34
34
|
|
|
35
|
-
1. **extract**: SPEC
|
|
36
|
-
2. **check**:
|
|
37
|
-
3. **diff**:
|
|
35
|
+
1. **extract**: parse SPEC sections like `## API` / `## Endpoints` / `## Interface` and persist as a structured contract record
|
|
36
|
+
2. **check**: locate matching endpoints in the implementation, compare signature/schema, report drift as P1 findings
|
|
37
|
+
3. **diff**: compare against the previous snapshot, surface only **changed fields** (noise minimized)
|
|
38
38
|
|
|
39
39
|
## Drift severity
|
|
40
40
|
|
|
41
|
-
| Drift type | Severity |
|
|
41
|
+
| Drift type | Severity | Example |
|
|
42
42
|
|---|---|---|
|
|
43
|
-
| Missing endpoint | P1 | SPEC
|
|
44
|
-
| Missing required field in response | P1 | SPEC response
|
|
43
|
+
| Missing endpoint | P1 | SPEC says `GET /users/:id`, implementation has none |
|
|
44
|
+
| Missing required field in response | P1 | SPEC response includes `email`, implementation drops it |
|
|
45
45
|
| Type change (breaking) | P1 | `userId: number` → `userId: string` |
|
|
46
|
-
| Added required request field | P1 |
|
|
47
|
-
| Added optional field | P3 |
|
|
48
|
-
| Status code added | P2 |
|
|
49
|
-
| Status code removed | P1 |
|
|
46
|
+
| Added required request field | P1 | breaks existing clients |
|
|
47
|
+
| Added optional field | P3 | extension is allowed |
|
|
48
|
+
| Status code added | P2 | client must handle a new case |
|
|
49
|
+
| Status code removed | P1 | expected response disappeared |
|
|
50
50
|
|
|
51
|
-
**P1
|
|
51
|
+
**On any P1 drift**: treat as failure regardless of `/vibe.verify` outcome — tests can pass while the contract breaks.
|
|
52
52
|
|
|
53
53
|
## Storage Format
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
.claude/vibe/contracts/
|
|
57
|
-
<feature>.md #
|
|
58
|
-
<feature>.snapshot.md #
|
|
57
|
+
<feature>.md # extracted contract (SSOT)
|
|
58
|
+
<feature>.snapshot.md # implementation snapshot at last check (for diff)
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### Contract schema (frontmatter)
|
|
@@ -81,24 +81,24 @@ endpoints:
|
|
|
81
81
|
|
|
82
82
|
## Integration with /vibe.verify
|
|
83
83
|
|
|
84
|
-
`/vibe.verify <feature>`
|
|
84
|
+
After `/vibe.verify <feature>` scenarios pass, auto-chain:
|
|
85
85
|
|
|
86
86
|
```
|
|
87
87
|
scenarios pass → /vibe.contract check <feature>
|
|
88
88
|
├─ no drift → ✅ complete
|
|
89
|
-
└─ drift found → ❌ report + auto
|
|
89
|
+
└─ drift found → ❌ report + auto /vibe.regress register (tag: integration)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Integration with /vibe.spec
|
|
93
93
|
|
|
94
|
-
`/vibe.spec`
|
|
94
|
+
Right after `/vibe.spec` finishes writing the SPEC, auto-invoke `/vibe.contract extract`. The resulting contract becomes the reference for the subsequent `/vibe.run`.
|
|
95
95
|
|
|
96
96
|
## Done Criteria
|
|
97
97
|
|
|
98
|
-
- [ ] `extract
|
|
99
|
-
- [ ] `check
|
|
100
|
-
- [ ] P1
|
|
101
|
-
- [ ] `diff
|
|
98
|
+
- [ ] `extract` exits cleanly when SPEC has no API section (not every feature has one)
|
|
99
|
+
- [ ] `check` is silent when no drift; otherwise prints findings grouped by severity
|
|
100
|
+
- [ ] Every P1 drift triggers `/vibe.regress register --from-contract`
|
|
101
|
+
- [ ] `diff` says "first run" when no prior snapshot exists
|
|
102
102
|
|
|
103
103
|
---
|
|
104
104
|
|
package/commands/vibe.regress.md
CHANGED
|
@@ -5,51 +5,51 @@ argument-hint: "register | generate | list | import | cluster [args]"
|
|
|
5
5
|
|
|
6
6
|
# /vibe.regress
|
|
7
7
|
|
|
8
|
-
**Regression Auto-Evolution** —
|
|
8
|
+
**Regression Auto-Evolution** — never fix the same bug twice.
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> Bugs are recorded, preventive tests are generated automatically, and recurring patterns get promoted into shared tests.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
/vibe.regress register "<symptom>" #
|
|
16
|
-
/vibe.regress generate <slug> # bug → vitest
|
|
17
|
-
/vibe.regress list #
|
|
18
|
-
/vibe.regress import # git log
|
|
19
|
-
/vibe.regress cluster # 3+
|
|
15
|
+
/vibe.regress register "<symptom>" # Manual register (rare — most calls are automatic)
|
|
16
|
+
/vibe.regress generate <slug> # bug record → vitest file
|
|
17
|
+
/vibe.regress list # Open items
|
|
18
|
+
/vibe.regress import # Backfill from git log `fix:` commits
|
|
19
|
+
/vibe.regress cluster # 3+ similar bugs → propose shared test
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Auto-integration
|
|
23
23
|
|
|
24
|
-
- `/vibe.verify`
|
|
25
|
-
- `/vibe.run "<feature>"`
|
|
24
|
+
- `/vibe.verify` failure → auto-invokes `register` (no manual step)
|
|
25
|
+
- `/vibe.run "<feature>"` start → warns about open regressions for that feature
|
|
26
26
|
|
|
27
27
|
## Process
|
|
28
28
|
|
|
29
29
|
Load skill `vibe-regress` with subcommand: `$ARGUMENTS`
|
|
30
30
|
|
|
31
|
-
`vibe-regress`
|
|
31
|
+
The `vibe-regress` skill performs registration, generation, and clustering.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
**Core steps** (see `skills/vibe-regress/SKILL.md` for details):
|
|
34
34
|
|
|
35
|
-
1.
|
|
36
|
-
2. `.claude/vibe/regressions/<slug>.md`
|
|
37
|
-
3. `generate
|
|
38
|
-
4. `cluster
|
|
39
|
-
5. `import
|
|
35
|
+
1. Parse subcommand
|
|
36
|
+
2. Read/write `.claude/vibe/regressions/<slug>.md` (frontmatter schema enforced)
|
|
37
|
+
3. On `generate`, detect the project's test stack → choose template (vitest / jest)
|
|
38
|
+
4. On `cluster`, group by `root-cause-tag`; ≥3 entries → propose a shared test
|
|
39
|
+
5. On `import`, parse `git log --grep='^fix:'`; skip duplicates by commit hash
|
|
40
40
|
|
|
41
41
|
## Output
|
|
42
42
|
|
|
43
|
-
- `.claude/vibe/regressions/<slug>.md` —
|
|
44
|
-
-
|
|
45
|
-
- `list`
|
|
43
|
+
- `.claude/vibe/regressions/<slug>.md` — bug record (frontmatter + reproduction / root cause)
|
|
44
|
+
- Project test dir — generated vitest file (`*.regression.test.ts`)
|
|
45
|
+
- `list` prints a terminal table
|
|
46
46
|
|
|
47
47
|
## Storage Format
|
|
48
48
|
|
|
49
49
|
```markdown
|
|
50
50
|
---
|
|
51
51
|
slug: login-jwt-expiry-off-by-one
|
|
52
|
-
symptom: "JWT
|
|
52
|
+
symptom: "JWT expiry cuts off one second early"
|
|
53
53
|
root-cause-tag: timezone
|
|
54
54
|
fix-commit: abc1234
|
|
55
55
|
test-path: src/auth/__tests__/login.regression.test.ts
|
package/commands/vibe.run.md
CHANGED
|
@@ -48,18 +48,18 @@ Execute **Scenario-Driven Implementation** with automatic quality verification.
|
|
|
48
48
|
|
|
49
49
|
### Pre-Run Regression Check (MANDATORY, before implementation starts)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Run immediately after start:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
Load skill `vibe-regress` with: list --feature "{feature-name}"
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
-
|
|
58
|
-
- interactive
|
|
59
|
-
- ultrawork
|
|
60
|
-
-
|
|
57
|
+
- If any open regressions exist:
|
|
58
|
+
- interactive mode: ask the user "generate preventive tests first, then proceed?"
|
|
59
|
+
- ultrawork mode: auto-invoke `/vibe.regress generate <slug>` for each, then proceed
|
|
60
|
+
- No open regressions → silently continue
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Also load `.claude/vibe/contracts/{feature-name}.md` if present — use it as the contract reference during implementation.
|
|
63
63
|
|
|
64
64
|
### Core Flow
|
|
65
65
|
|
package/commands/vibe.spec.md
CHANGED
|
@@ -372,15 +372,15 @@ Load skill `vibe-spec-review` with feature: {feature-name}
|
|
|
372
372
|
5. Review Debate Team (2+ P1/P2 이슈 시)
|
|
373
373
|
6. 사용자 최종 체크포인트
|
|
374
374
|
|
|
375
|
-
### Phase 4.5: Contract Extract (
|
|
375
|
+
### Phase 4.5: Contract Extract (auto, only for features with an API)
|
|
376
376
|
|
|
377
377
|
```
|
|
378
378
|
Load skill `vibe-contract` with: extract "{feature-name}"
|
|
379
379
|
```
|
|
380
380
|
|
|
381
|
-
SPEC
|
|
381
|
+
If the SPEC has a `## API` / `## Endpoints` / `## Interface` section, extract the contract to `.claude/vibe/contracts/{feature-name}.md`. If the section is absent, exit cleanly (not every feature has an API).
|
|
382
382
|
|
|
383
|
-
|
|
383
|
+
The contract is referenced during Phase 5a implementation, and used by `/vibe.verify` for drift detection.
|
|
384
384
|
|
|
385
385
|
### Phase 5a: Logic Track
|
|
386
386
|
|
|
@@ -388,9 +388,9 @@ SPEC에 `## API` / `## Endpoints` / `## Interface` 섹션이 있으면 계약을
|
|
|
388
388
|
/vibe.run "{feature-name}"
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
SPEC →
|
|
392
|
-
- `/vibe.regress list --feature {feature-name}` —
|
|
393
|
-
- `.claude/vibe/contracts/{feature-name}.md` —
|
|
391
|
+
SPEC → code. Auto-checks at start:
|
|
392
|
+
- `/vibe.regress list --feature {feature-name}` — warn if any open regressions exist
|
|
393
|
+
- `.claude/vibe/contracts/{feature-name}.md` — load if present, use as implementation guide
|
|
394
394
|
|
|
395
395
|
### Phase 5b: UI Track (type ∈ {website, webapp, mobile}일 때만)
|
|
396
396
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Self-test vibe across CC and coco — verify every command/skill/hook/agent/tool is callable and behaves identically
|
|
3
|
+
argument-hint: "parity | report | compare [args]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /vibe.test
|
|
7
|
+
|
|
8
|
+
**Vibe Self-Test** — verify vibe works identically in both Claude Code and coco.
|
|
9
|
+
|
|
10
|
+
> Catch features broken on one harness before users do.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/vibe.test parity # Static parity (file set + content sync) — local, fast
|
|
16
|
+
/vibe.test report # Invoke every feature in current harness, write JSON+MD report
|
|
17
|
+
/vibe.test compare <cc-report> <coco-report> # Diff two reports, classify P1/P2/P3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Key Constraint
|
|
21
|
+
|
|
22
|
+
`/vibe.test report` only tests the **harness it runs in**. Run from CC for CC results, run from coco for coco results. Then `compare` merges them.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
[CC] /vibe.test report → .claude/vibe/test-reports/<ts>-cc.{json,md}
|
|
26
|
+
[coco] /vibe.test report → .coco/vibe/test-reports/<ts>-coco.{json,md}
|
|
27
|
+
[any] /vibe.test compare → diff with parity findings
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Subcommand: parity (static check, stage 1)
|
|
31
|
+
|
|
32
|
+
No harness execution — file system comparison only:
|
|
33
|
+
|
|
34
|
+
| Check | Compared |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **install set** | `~/.claude/{commands,skills,agents}/` vs `~/.coco/{commands,skills,agents}/` file set |
|
|
37
|
+
| **content sync** | `CLAUDE.md` ↔ `AGENTS.md` body (excluding header/meta blocks) |
|
|
38
|
+
| **path config** | `.claude/vibe/` vs `.coco/vibe/` directory layout |
|
|
39
|
+
| **doc references** | Paths cited in CLAUDE.md/AGENTS.md actually resolve in install dir |
|
|
40
|
+
|
|
41
|
+
**Output**: console table + `.claude/vibe/test-reports/<ts>-parity.json`
|
|
42
|
+
|
|
43
|
+
This stage alone catches:
|
|
44
|
+
- New commands missing on one harness (e.g. if `/vibe.regress` had been added only to CC)
|
|
45
|
+
- AGENTS.md holding stale paths (e.g. `.codex/` references after a coco rename)
|
|
46
|
+
- CLAUDE.md ↔ AGENTS.md body drift
|
|
47
|
+
|
|
48
|
+
## Subcommand: report (runtime invocation)
|
|
49
|
+
|
|
50
|
+
Probes every shipped feature in the current harness and writes a JSON+MD report.
|
|
51
|
+
|
|
52
|
+
| Category | Probe |
|
|
53
|
+
|---|---|
|
|
54
|
+
| commands | frontmatter validity, body delegates to a skill |
|
|
55
|
+
| skills | frontmatter validity, triggers non-empty |
|
|
56
|
+
| hooks | run matching vitest suite |
|
|
57
|
+
| agents | frontmatter validity, declared tools exist in harness |
|
|
58
|
+
| tools | run matching vitest suite or smoke-call with minimal input |
|
|
59
|
+
|
|
60
|
+
No external LLM calls. Interactive commands are not actually invoked — structural validation only. See `skills/vibe-test/SKILL.md` for full probe spec and failure-handling rules.
|
|
61
|
+
|
|
62
|
+
## Subcommand: compare (diff two reports)
|
|
63
|
+
|
|
64
|
+
Compare two JSON reports and classify findings:
|
|
65
|
+
- **P1**: feature exists on only one side → missing
|
|
66
|
+
- **P2**: both sides have it but response shape differs → behavioral drift
|
|
67
|
+
- **P3**: only message wording differs, semantics identical → informational
|
|
68
|
+
|
|
69
|
+
P1 findings auto-invoke `/vibe.regress register --from-test`.
|
|
70
|
+
|
|
71
|
+
## Process
|
|
72
|
+
|
|
73
|
+
Load skill `vibe-test` with subcommand: `$ARGUMENTS`
|
|
74
|
+
|
|
75
|
+
See `skills/vibe-test/SKILL.md` for detailed logic.
|
|
76
|
+
|
|
77
|
+
## Storage
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
.claude/vibe/test-reports/ (CC side)
|
|
81
|
+
.coco/vibe/test-reports/ (coco side)
|
|
82
|
+
<YYYYMMDD-HHmm>-<harness>.json
|
|
83
|
+
<YYYYMMDD-HHmm>-<harness>.md
|
|
84
|
+
<YYYYMMDD-HHmm>-compare.md (compare output)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Done Criteria
|
|
88
|
+
|
|
89
|
+
- [ ] `parity` runs without external calls — local file inspection only (fast, deterministic)
|
|
90
|
+
- [ ] If only one install dir exists, exit cleanly with guidance (not an error)
|
|
91
|
+
- [ ] `compare` warns when reports are not within ±1 minute of each other (timing drift = false positives)
|
|
92
|
+
- [ ] P1 drift auto-registers via `/vibe.regress`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
ARGUMENTS: $ARGUMENTS
|
package/commands/vibe.verify.md
CHANGED
|
@@ -235,9 +235,9 @@ For each failed scenario:
|
|
|
235
235
|
location: {file:line}
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
- `--from-verify`
|
|
239
|
-
-
|
|
240
|
-
-
|
|
238
|
+
- `--from-verify` mode skips user confirmation (the user is already attentive in a verify-failure context; minimize friction)
|
|
239
|
+
- The registered bug's slug appears as a link in the Failure Report's "Fix" section
|
|
240
|
+
- Follow up with `/vibe.regress generate <slug>` to produce a preventive test
|
|
241
241
|
|
|
242
242
|
### Failure Report
|
|
243
243
|
|
|
@@ -402,18 +402,18 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
|
|
|
402
402
|
**Codex P2 발견 시:**
|
|
403
403
|
- TODO 파일에 기록 후 완료 처리
|
|
404
404
|
|
|
405
|
-
## Post-Verify Contract Check (
|
|
405
|
+
## Post-Verify Contract Check (auto, only when a contract file exists)
|
|
406
406
|
|
|
407
|
-
|
|
407
|
+
After all scenarios pass, auto-invoke:
|
|
408
408
|
|
|
409
409
|
```
|
|
410
410
|
Load skill `vibe-contract` with: check "{feature-name}"
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
-
- `.claude/vibe/contracts/{feature-name}.md
|
|
414
|
-
- drift
|
|
415
|
-
- **P1 drift** → verify
|
|
416
|
-
- P2/P3 drift →
|
|
413
|
+
- Skip if `.claude/vibe/contracts/{feature-name}.md` does not exist
|
|
414
|
+
- No drift → verify still passes
|
|
415
|
+
- **P1 drift** → demote verify to fail; auto-call `/vibe.regress register --from-contract`
|
|
416
|
+
- P2 / P3 drift → warning only; verify still passes
|
|
417
417
|
|
|
418
418
|
## Next Step
|
|
419
419
|
|
|
@@ -280,8 +280,89 @@ describe('pre-tool-guard', () => {
|
|
|
280
280
|
tool_name: 'Bash',
|
|
281
281
|
tool_input: { command: 'DROP TABLE users' },
|
|
282
282
|
});
|
|
283
|
-
// tool_input is stringified — pattern matches against the JSON string
|
|
284
283
|
expect(result.exitCode).toBe(2);
|
|
284
|
+
expect(result.stdout).toContain('Database drop detected');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ══════════════════════════════════════════════════
|
|
289
|
+
// Regression: file content false positives (issue: machine-key.ts blocked)
|
|
290
|
+
// .claude/vibe/regressions/pre-tool-guard-content-false-positive.md
|
|
291
|
+
//
|
|
292
|
+
// 이전 구현은 tool_input 전체를 JSON.stringify해서 패턴 매칭했기 때문에
|
|
293
|
+
// 파일 내용에 '/etc/', '.env', 'secret' 같은 리터럴이 있으면 차단됐음.
|
|
294
|
+
// write/edit 패턴은 file_path만 봐야 한다.
|
|
295
|
+
// ══════════════════════════════════════════════════
|
|
296
|
+
describe('regression: write/edit content must not trigger path patterns', () => {
|
|
297
|
+
it('should ALLOW writing safe path even when content contains "/etc/" literal', () => {
|
|
298
|
+
const result = runGuardWithStdin({
|
|
299
|
+
tool_name: 'Write',
|
|
300
|
+
tool_input: {
|
|
301
|
+
file_path: 'src/machine-key.ts',
|
|
302
|
+
content: "for (const path of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {}",
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
expect(result.exitCode).toBe(0);
|
|
306
|
+
expect(result.stdout).not.toContain('Writing to system directory');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should ALLOW writing safe path even when content contains "/usr/" literal', () => {
|
|
310
|
+
const result = runGuardWithStdin({
|
|
311
|
+
tool_name: 'Write',
|
|
312
|
+
tool_input: {
|
|
313
|
+
file_path: 'src/cli-detect.ts',
|
|
314
|
+
content: "const IOREG = '/usr/sbin/ioreg';",
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
expect(result.exitCode).toBe(0);
|
|
318
|
+
expect(result.stdout).not.toContain('Writing to system directory');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should ALLOW writing safe path even when content mentions ".env" / "secret"', () => {
|
|
322
|
+
const result = runGuardWithStdin({
|
|
323
|
+
tool_name: 'Write',
|
|
324
|
+
tool_input: {
|
|
325
|
+
file_path: 'src/config.ts',
|
|
326
|
+
content: "// loads from .env, never log secret values",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
expect(result.exitCode).toBe(0);
|
|
330
|
+
expect(result.stdout).not.toContain('Writing to sensitive file');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should ALLOW editing safe path even when new_string contains ".env" literal', () => {
|
|
334
|
+
const result = runGuardWithStdin({
|
|
335
|
+
tool_name: 'Edit',
|
|
336
|
+
tool_input: {
|
|
337
|
+
file_path: 'src/index.ts',
|
|
338
|
+
old_string: 'const x = 1',
|
|
339
|
+
new_string: '// reads .env at startup',
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
expect(result.exitCode).toBe(0);
|
|
343
|
+
expect(result.stdout).not.toContain('Editing sensitive file');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should still BLOCK Write when file_path itself targets /etc/', () => {
|
|
347
|
+
const result = runGuardWithStdin({
|
|
348
|
+
tool_name: 'Write',
|
|
349
|
+
tool_input: { file_path: '/etc/passwd', content: 'root:x:0:0' },
|
|
350
|
+
});
|
|
351
|
+
expect(result.exitCode).toBe(2);
|
|
352
|
+
expect(result.stdout).toContain('Writing to system directory');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should still WARN Edit when file_path itself is a credentials file', () => {
|
|
356
|
+
const result = runGuardWithStdin({
|
|
357
|
+
tool_name: 'Edit',
|
|
358
|
+
tool_input: {
|
|
359
|
+
file_path: 'config/credentials.json',
|
|
360
|
+
old_string: 'a',
|
|
361
|
+
new_string: 'b',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
expect(result.exitCode).toBe(0);
|
|
365
|
+
expect(result.stdout).toContain('Editing sensitive file');
|
|
285
366
|
});
|
|
286
367
|
});
|
|
287
368
|
});
|
|
@@ -7,26 +7,35 @@
|
|
|
7
7
|
import { VIBE_PATH, PROJECT_DIR } from './utils.js';
|
|
8
8
|
|
|
9
9
|
// 위험한 명령어 패턴
|
|
10
|
+
//
|
|
11
|
+
// 각 엔트리의 `target`은 매칭 대상 필드:
|
|
12
|
+
// - 'command' → tool_input.command (Bash)
|
|
13
|
+
// - 'file_path' → tool_input.file_path (Write, Edit)
|
|
14
|
+
// - 'raw' → 전체 문자열 (스키마 모를 때 폴백)
|
|
15
|
+
//
|
|
16
|
+
// 예전 버전은 항상 stringify된 tool_input 전체에 매칭하여 file content가
|
|
17
|
+
// 패턴(예: /etc/, .env)과 일치하면 false positive로 차단되는 버그가 있었음.
|
|
18
|
+
// 회귀: .claude/vibe/regressions/pre-tool-guard-content-false-positive.md
|
|
10
19
|
const DANGEROUS_PATTERNS = {
|
|
11
20
|
bash: [
|
|
12
|
-
{ pattern: /rm\s+-rf?\s+[\/~]/, severity: 'critical', message: 'Deleting root or home directory' },
|
|
13
|
-
{ pattern: /rm\s+-rf?\s+\*/, severity: 'high', message: 'Wildcard deletion detected' },
|
|
14
|
-
{ pattern: /git\s+push\s+.*--force/, severity: 'high', message: 'Force push detected' },
|
|
15
|
-
{ pattern: /git\s+reset\s+--hard/, severity: 'medium', message: 'Hard reset will discard changes' },
|
|
16
|
-
{ pattern: /drop\s+(table|database)/i, severity: 'critical', message: 'Database drop detected' },
|
|
17
|
-
{ pattern: /truncate\s+table/i, severity: 'high', message: 'Table truncate detected' },
|
|
18
|
-
{ pattern: /:(){ :|:& };:/, severity: 'critical', message: 'Fork bomb detected' },
|
|
19
|
-
{ pattern: /mkfs|fdisk|dd\s+if=/, severity: 'critical', message: 'Disk operation detected' },
|
|
20
|
-
{ pattern: /chmod\s+-R\s+777/, severity: 'medium', message: 'Insecure permission change' },
|
|
21
|
-
{ pattern: /curl.*\|\s*(ba)?sh/, severity: 'high', message: 'Piping curl to shell' },
|
|
21
|
+
{ pattern: /rm\s+-rf?\s+[\/~]/, target: 'command', severity: 'critical', message: 'Deleting root or home directory' },
|
|
22
|
+
{ pattern: /rm\s+-rf?\s+\*/, target: 'command', severity: 'high', message: 'Wildcard deletion detected' },
|
|
23
|
+
{ pattern: /git\s+push\s+.*--force/, target: 'command', severity: 'high', message: 'Force push detected' },
|
|
24
|
+
{ pattern: /git\s+reset\s+--hard/, target: 'command', severity: 'medium', message: 'Hard reset will discard changes' },
|
|
25
|
+
{ pattern: /drop\s+(table|database)/i, target: 'command', severity: 'critical', message: 'Database drop detected' },
|
|
26
|
+
{ pattern: /truncate\s+table/i, target: 'command', severity: 'high', message: 'Table truncate detected' },
|
|
27
|
+
{ pattern: /:(){ :|:& };:/, target: 'command', severity: 'critical', message: 'Fork bomb detected' },
|
|
28
|
+
{ pattern: /mkfs|fdisk|dd\s+if=/, target: 'command', severity: 'critical', message: 'Disk operation detected' },
|
|
29
|
+
{ pattern: /chmod\s+-R\s+777/, target: 'command', severity: 'medium', message: 'Insecure permission change' },
|
|
30
|
+
{ pattern: /curl.*\|\s*(ba)?sh/, target: 'command', severity: 'high', message: 'Piping curl to shell' },
|
|
22
31
|
],
|
|
23
32
|
edit: [
|
|
24
|
-
{ pattern: /\.env|credentials|secret|password|api[_-]?key/i, severity: 'medium', message: 'Editing sensitive file' },
|
|
25
|
-
{ pattern: /package-lock\.json|yarn\.lock|pnpm-lock/, severity: 'low', message: 'Editing lock file directly' },
|
|
33
|
+
{ pattern: /\.env|credentials|secret|password|api[_-]?key/i, target: 'file_path', severity: 'medium', message: 'Editing sensitive file' },
|
|
34
|
+
{ pattern: /package-lock\.json|yarn\.lock|pnpm-lock/, target: 'file_path', severity: 'low', message: 'Editing lock file directly' },
|
|
26
35
|
],
|
|
27
36
|
write: [
|
|
28
|
-
{ pattern: /\.env|credentials|secret/i, severity: 'medium', message: 'Writing to sensitive file' },
|
|
29
|
-
{ pattern: /\/etc\/|\/usr\/|C:\\Windows/i, severity: 'critical', message: 'Writing to system directory' },
|
|
37
|
+
{ pattern: /\.env|credentials|secret/i, target: 'file_path', severity: 'medium', message: 'Writing to sensitive file' },
|
|
38
|
+
{ pattern: /\/etc\/|\/usr\/|C:\\Windows/i, target: 'file_path', severity: 'critical', message: 'Writing to system directory' },
|
|
30
39
|
],
|
|
31
40
|
};
|
|
32
41
|
|
|
@@ -82,10 +91,39 @@ const SAFE_ALTERNATIVES = {
|
|
|
82
91
|
'chmod 777': 'Use specific permissions (e.g., chmod 755 for directories)',
|
|
83
92
|
};
|
|
84
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 패턴 매칭 대상 추출
|
|
96
|
+
*
|
|
97
|
+
* tool_input이 객체이면 target 필드만 뽑아 반환. 객체가 아니거나(레거시 argv 모드)
|
|
98
|
+
* target='raw'이면 입력 전체를 그대로 반환.
|
|
99
|
+
*
|
|
100
|
+
* 핵심: write/edit 패턴은 file_path만 봐야 한다. content에 우연히 들어간
|
|
101
|
+
* 리터럴(예: 코드 안의 '/etc/machine-id' 문자열)이 차단을 일으키지 않도록.
|
|
102
|
+
*/
|
|
103
|
+
function extractTarget(rawInput, target) {
|
|
104
|
+
if (target === 'raw') return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
|
105
|
+
|
|
106
|
+
// 객체로 파싱 시도
|
|
107
|
+
let obj = rawInput;
|
|
108
|
+
if (typeof rawInput === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
obj = JSON.parse(rawInput);
|
|
111
|
+
} catch {
|
|
112
|
+
// JSON 아님 → 레거시 argv 모드. argv[3]은 보통 file_path 또는 command 단일 문자열.
|
|
113
|
+
// 안전하게 그 문자열을 target 값으로 간주.
|
|
114
|
+
return rawInput;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof obj !== 'object' || obj === null) return '';
|
|
119
|
+
const value = obj[target];
|
|
120
|
+
return typeof value === 'string' ? value : '';
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
/**
|
|
86
124
|
* 명령어 검증
|
|
87
125
|
*/
|
|
88
|
-
function validateCommand(toolName,
|
|
126
|
+
function validateCommand(toolName, rawInput) {
|
|
89
127
|
const results = {
|
|
90
128
|
allowed: true,
|
|
91
129
|
severity: 'none',
|
|
@@ -95,8 +133,11 @@ function validateCommand(toolName, input) {
|
|
|
95
133
|
|
|
96
134
|
const patterns = DANGEROUS_PATTERNS[toolName.toLowerCase()] || [];
|
|
97
135
|
|
|
98
|
-
for (const { pattern, severity, message } of patterns) {
|
|
99
|
-
|
|
136
|
+
for (const { pattern, target, severity, message } of patterns) {
|
|
137
|
+
const haystack = extractTarget(rawInput, target || 'raw');
|
|
138
|
+
if (!haystack) continue;
|
|
139
|
+
|
|
140
|
+
if (pattern.test(haystack)) {
|
|
100
141
|
results.warnings.push(`[${severity.toUpperCase()}] ${message}`);
|
|
101
142
|
|
|
102
143
|
// 심각도에 따른 처리
|
|
@@ -109,9 +150,9 @@ function validateCommand(toolName, input) {
|
|
|
109
150
|
results.severity = severity;
|
|
110
151
|
}
|
|
111
152
|
|
|
112
|
-
// 대안 제안
|
|
153
|
+
// 대안 제안 — 매칭된 target 필드에서만 검색
|
|
113
154
|
for (const [dangerous, safe] of Object.entries(SAFE_ALTERNATIVES)) {
|
|
114
|
-
if (
|
|
155
|
+
if (haystack.includes(dangerous)) {
|
|
115
156
|
results.suggestions.push(safe);
|
|
116
157
|
}
|
|
117
158
|
}
|