cortexhawk 3.2.0 → 3.3.1
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/.cortexhawk-lint.yml.example +21 -0
- package/.gitmessage +10 -0
- package/CHANGELOG.md +45 -0
- package/CLAUDE.md +12 -4
- package/agents/git-manager.md +6 -2
- package/commands/backlog.md +1 -1
- package/commands/cleanup.md +37 -0
- package/commands/review-pr.md +31 -0
- package/commands/ship.md +1 -0
- package/commands/task.md +1 -1
- package/cortexhawk +9 -3
- package/hooks/branch-guard.sh +8 -1
- package/hooks/codex-dispatcher.sh +3 -0
- package/hooks/compose.yml +6 -0
- package/hooks/file-guard.sh +4 -0
- package/hooks/hooks.json +6 -0
- package/hooks/lint-guard.sh +46 -0
- package/hooks/post-merge.sh +12 -0
- package/hooks/session-start.sh +1 -1
- package/install.sh +159 -962
- package/mcp/README.md +36 -0
- package/mcp/context7.json +1 -1
- package/mcp/github.json +11 -0
- package/mcp/puppeteer.json +1 -1
- package/mcp/sequential-thinking.json +1 -1
- package/package.json +1 -1
- package/profiles/api.json +2 -1
- package/profiles/fullstack.json +2 -1
- package/scripts/autodetect-profile.sh +1 -1
- package/scripts/doctor.sh +164 -0
- package/scripts/install-claude.sh +179 -0
- package/scripts/interactive-init.sh +3 -2
- package/scripts/lint-guard-runner.sh +132 -0
- package/scripts/post-merge-cleanup.sh +233 -0
- package/scripts/restore.sh +212 -0
- package/scripts/snapshot.sh +163 -0
- package/scripts/update.sh +280 -0
- package/settings.json +12 -1
package/mcp/README.md
CHANGED
|
@@ -15,6 +15,7 @@ cp mcp/context7.json .mcp.json
|
|
|
15
15
|
|
|
16
16
|
| Server | Purpose | Requires |
|
|
17
17
|
|---|---|---|
|
|
18
|
+
| `github.json` | GitHub API — PRs, issues, reviews, repos | npx + `GITHUB_PERSONAL_ACCESS_TOKEN` env |
|
|
18
19
|
| `context7.json` | Library docs lookup via Context7 | npx |
|
|
19
20
|
| `sequential-thinking.json` | Step-by-step reasoning for complex problems | npx |
|
|
20
21
|
| `puppeteer.json` | Browser automation and testing | npx |
|
|
@@ -35,3 +36,38 @@ Create or edit `.mcp.json` at your project root:
|
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
Then restart Claude Code to activate.
|
|
39
|
+
|
|
40
|
+
## GitHub Token Setup
|
|
41
|
+
|
|
42
|
+
`github.json` requires a `GITHUB_PERSONAL_ACCESS_TOKEN`. Options (pick one):
|
|
43
|
+
|
|
44
|
+
**Option 1 — `gh auth token` (recommended, zero extra credentials)**
|
|
45
|
+
```bash
|
|
46
|
+
# ~/.zshrc or ~/.bashrc
|
|
47
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=$(gh auth token)
|
|
48
|
+
```
|
|
49
|
+
Reuses existing `gh` CLI auth — nothing new to manage.
|
|
50
|
+
|
|
51
|
+
**Option 2 — `.claude/settings.json` (project-level, gitignored)**
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"env": {
|
|
55
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Option 3 — Shell profile (direct)**
|
|
61
|
+
```bash
|
|
62
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxxx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Option 4 — `direnv` (`.envrc`, add to `.gitignore`)**
|
|
66
|
+
```bash
|
|
67
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxxx
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Option 5 — Password manager CLI**
|
|
71
|
+
```bash
|
|
72
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=$(op read "op://vault/github-pat/token")
|
|
73
|
+
```
|
package/mcp/context7.json
CHANGED
package/mcp/github.json
ADDED
package/mcp/puppeteer.json
CHANGED
package/package.json
CHANGED
package/profiles/api.json
CHANGED
package/profiles/fullstack.json
CHANGED
|
@@ -46,7 +46,7 @@ for skill_file in "$CORTEXHAWK_DIR"/skills/*/*/SKILL.md; do
|
|
|
46
46
|
done
|
|
47
47
|
|
|
48
48
|
# --- Generate JSON ---
|
|
49
|
-
PROFILE_FILE
|
|
49
|
+
PROFILE_FILE=$(mktemp /tmp/cortexhawk-autodetect-XXXXXX)
|
|
50
50
|
SKILL_JSON=$(echo "$SKILLS" | tr ',' '\n' | sed 's/.*/ "&"/' | paste -sd ',' - | sed 's/,/,\n/g')
|
|
51
51
|
cat > "$PROFILE_FILE" << EOF
|
|
52
52
|
{
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# doctor.sh — CortexHawk installation health diagnostics
|
|
3
|
+
# Sourced by install.sh when --doctor is used
|
|
4
|
+
# Uses shared functions: get_version, green, yellow
|
|
5
|
+
# Uses globals: GLOBAL, TARGET, SCRIPT_DIR
|
|
6
|
+
|
|
7
|
+
do_doctor() {
|
|
8
|
+
if [ "$GLOBAL" = true ]; then
|
|
9
|
+
TARGET="$HOME/.claude"
|
|
10
|
+
else
|
|
11
|
+
TARGET="$(pwd)/.claude"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
local ok=0 warn=0 err=0
|
|
15
|
+
_doc_ok() { echo " [OK] $1"; ok=$((ok+1)); }
|
|
16
|
+
_doc_warn() { echo " [WARN] $1"; warn=$((warn+1)); }
|
|
17
|
+
_doc_err() { echo " [ERR] $1"; err=$((err+1)); }
|
|
18
|
+
|
|
19
|
+
# Header
|
|
20
|
+
local version="" profile="" target_cli_name=""
|
|
21
|
+
if [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
22
|
+
version=$(grep -o '"version": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
23
|
+
profile=$(grep -o '"profile": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
24
|
+
target_cli_name=$(grep -o '"target": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
25
|
+
fi
|
|
26
|
+
echo "CortexHawk Doctor"
|
|
27
|
+
echo "==================="
|
|
28
|
+
echo " Installation: $TARGET"
|
|
29
|
+
echo " Version: ${version:-unknown}"
|
|
30
|
+
echo " Profile: ${profile:-unknown}"
|
|
31
|
+
echo " Target: ${target_cli_name:-claude}"
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Checks:"
|
|
34
|
+
|
|
35
|
+
# 1. Manifest
|
|
36
|
+
if [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
37
|
+
_doc_ok "Manifest present"
|
|
38
|
+
else
|
|
39
|
+
_doc_err "Manifest missing — run install.sh to create an installation"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# 2. settings.json valid JSON
|
|
43
|
+
if [ -f "$TARGET/settings.json" ]; then
|
|
44
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
45
|
+
if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$TARGET/settings.json" 2>/dev/null; then
|
|
46
|
+
_doc_ok "settings.json valid JSON"
|
|
47
|
+
else
|
|
48
|
+
_doc_err "settings.json invalid JSON"
|
|
49
|
+
fi
|
|
50
|
+
else
|
|
51
|
+
_doc_warn "settings.json present (python3 not available for validation)"
|
|
52
|
+
fi
|
|
53
|
+
else
|
|
54
|
+
_doc_warn "settings.json not found"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# 3. Component counts (compare installed vs source)
|
|
58
|
+
for comp in agents commands modes; do
|
|
59
|
+
local installed=0 source_count=0
|
|
60
|
+
installed=$(find "$TARGET/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
61
|
+
source_count=$(find "$SCRIPT_DIR/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
62
|
+
if [ "$installed" -eq "$source_count" ] 2>/dev/null; then
|
|
63
|
+
_doc_ok "$installed/$source_count $comp installed"
|
|
64
|
+
elif [ "$installed" -gt 0 ] 2>/dev/null; then
|
|
65
|
+
_doc_warn "$installed/$source_count $comp installed"
|
|
66
|
+
else
|
|
67
|
+
_doc_err "0/$source_count $comp installed"
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
# 4. Skills (profile-dependent, just count what's there)
|
|
72
|
+
local skills_installed=0 skills_source=0
|
|
73
|
+
skills_installed=$(find "$TARGET/skills" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
74
|
+
skills_source=$(find "$SCRIPT_DIR/skills" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
75
|
+
if [ "$skills_installed" -gt 0 ] 2>/dev/null; then
|
|
76
|
+
_doc_ok "$skills_installed/$skills_source skills installed (profile: ${profile:-all})"
|
|
77
|
+
else
|
|
78
|
+
_doc_err "No skills installed"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# 5. Hooks executable
|
|
82
|
+
local hooks_ok=0 hooks_total=0
|
|
83
|
+
for hook in "$TARGET/hooks/"*.sh; do
|
|
84
|
+
[ -f "$hook" ] || continue
|
|
85
|
+
hooks_total=$((hooks_total+1))
|
|
86
|
+
if [ -x "$hook" ]; then
|
|
87
|
+
hooks_ok=$((hooks_ok+1))
|
|
88
|
+
else
|
|
89
|
+
_doc_warn "Hook not executable: $(basename "$hook")"
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
if [ "$hooks_total" -gt 0 ]; then
|
|
93
|
+
if [ "$hooks_ok" -eq "$hooks_total" ]; then
|
|
94
|
+
_doc_ok "$hooks_ok/$hooks_total hooks executable"
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
_doc_warn "No hooks found"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# 6. compose.yml vs settings.json coherence
|
|
101
|
+
if [ -f "$TARGET/../hooks/compose.yml" ] || [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
|
|
102
|
+
_doc_ok "compose.yml present"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# 7. MCP configs
|
|
106
|
+
if [ -d "$TARGET/mcp" ] && [ "$(find "$TARGET/mcp" -type f 2>/dev/null | wc -l)" -gt 0 ]; then
|
|
107
|
+
_doc_ok "MCP configs present"
|
|
108
|
+
elif [ -d "$TARGET/mcp" ]; then
|
|
109
|
+
_doc_warn "MCP directory exists but empty"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# 8. docs/ workspace
|
|
113
|
+
local project_root
|
|
114
|
+
project_root="$(dirname "$TARGET")"
|
|
115
|
+
if [ -d "$project_root/docs" ]; then
|
|
116
|
+
_doc_ok "docs/ workspace exists"
|
|
117
|
+
else
|
|
118
|
+
_doc_warn "docs/ workspace missing"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# 9. Broken symlinks in docs/plans/
|
|
122
|
+
local broken=0
|
|
123
|
+
if [ -d "$project_root/docs/plans" ]; then
|
|
124
|
+
while IFS= read -r link; do
|
|
125
|
+
[ -z "$link" ] && continue
|
|
126
|
+
_doc_warn "Broken symlink: $link"
|
|
127
|
+
broken=$((broken+1))
|
|
128
|
+
done < <(find "$project_root/docs/plans" -type l ! -exec test -e {} \; -print 2>/dev/null)
|
|
129
|
+
[ "$broken" -eq 0 ] && _doc_ok "No broken symlinks in docs/plans/"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# 10. git-workflow.conf
|
|
133
|
+
if [ -f "$TARGET/git-workflow.conf" ]; then
|
|
134
|
+
_doc_ok "git-workflow.conf present"
|
|
135
|
+
else
|
|
136
|
+
_doc_warn "git-workflow.conf not found (run --init to configure)"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# 11. CLAUDE.md at project root
|
|
140
|
+
if [ -f "$project_root/CLAUDE.md" ]; then
|
|
141
|
+
_doc_ok "CLAUDE.md present at project root"
|
|
142
|
+
else
|
|
143
|
+
_doc_warn "CLAUDE.md not found at project root"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# 12. Version match source vs manifest
|
|
147
|
+
if [ -n "$version" ]; then
|
|
148
|
+
local source_version
|
|
149
|
+
source_version=$(get_version)
|
|
150
|
+
if [ "$version" = "$source_version" ]; then
|
|
151
|
+
_doc_ok "Version match: source $source_version = manifest $version"
|
|
152
|
+
else
|
|
153
|
+
_doc_warn "Version mismatch: source $source_version != manifest $version (run --update)"
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Summary
|
|
158
|
+
echo ""
|
|
159
|
+
echo "Summary: $ok OK, $warn WARN, $err ERR"
|
|
160
|
+
|
|
161
|
+
# Exit code: 1 if any errors
|
|
162
|
+
[ "$err" -gt 0 ] && exit 1
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-claude.sh — CortexHawk installer for Claude Code
|
|
3
|
+
# Sourced by install.sh for fresh Claude Code installations
|
|
4
|
+
# Uses shared functions: copy_all_components, count_component_files, generate_hooks_config,
|
|
5
|
+
# write_manifest, create_docs_workspace, run_audit, update_gitignore, setup_templates,
|
|
6
|
+
# install_git_post_merge_hook, do_quickstart
|
|
7
|
+
|
|
8
|
+
install_claude() {
|
|
9
|
+
if [ "$GLOBAL" = true ]; then
|
|
10
|
+
TARGET="$HOME/.claude"
|
|
11
|
+
else
|
|
12
|
+
TARGET="$(pwd)/.claude"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [ "$DRY_RUN" = true ]; then
|
|
16
|
+
echo "CortexHawk Dry Run (install)"
|
|
17
|
+
echo "=============================="
|
|
18
|
+
echo " Target: $TARGET"
|
|
19
|
+
echo " Profile: ${PROFILE:-all}"
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Would install:"
|
|
22
|
+
count_component_files "$SCRIPT_DIR"
|
|
23
|
+
echo " settings.json"
|
|
24
|
+
[ ! -f "$(pwd)/CLAUDE.md" ] && echo " CLAUDE.md"
|
|
25
|
+
echo ""
|
|
26
|
+
echo "No files were modified (dry run)."
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "Installing for Claude Code to project: $TARGET"
|
|
31
|
+
|
|
32
|
+
copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
|
|
33
|
+
|
|
34
|
+
# Copy agent personas from project root if present
|
|
35
|
+
local project_root
|
|
36
|
+
project_root="$(dirname "$TARGET")"
|
|
37
|
+
if [ -d "$project_root/.cortexhawk-agents" ]; then
|
|
38
|
+
cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
|
|
39
|
+
local persona_count
|
|
40
|
+
persona_count=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
41
|
+
[ "$persona_count" -gt 0 ] && echo " Loaded $persona_count agent persona(s) from .cortexhawk-agents/"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
local hooks_json
|
|
45
|
+
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
46
|
+
if [ ! -f "$TARGET/settings.json" ]; then
|
|
47
|
+
# Fresh install: generate settings.json from scratch
|
|
48
|
+
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
49
|
+
echo "$hooks_json" | python3 -c "
|
|
50
|
+
import json, sys
|
|
51
|
+
hooks = json.load(sys.stdin)
|
|
52
|
+
with open(sys.argv[1]) as f:
|
|
53
|
+
permissions = json.load(f).get('permissions', {})
|
|
54
|
+
with open(sys.argv[2], 'w') as f:
|
|
55
|
+
json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
|
|
56
|
+
f.write('\n')
|
|
57
|
+
" "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
58
|
+
echo " Generated settings.json from hooks/compose.yml"
|
|
59
|
+
else
|
|
60
|
+
cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
61
|
+
fi
|
|
62
|
+
else
|
|
63
|
+
# Merge: preserve user customizations, add new hooks + permissions
|
|
64
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
65
|
+
echo " Warning: python3 not found — copying settings.json (merge skipped)"
|
|
66
|
+
cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
67
|
+
else
|
|
68
|
+
python3 -c "
|
|
69
|
+
import json, sys, shutil, os
|
|
70
|
+
|
|
71
|
+
raw = sys.stdin.read().strip()
|
|
72
|
+
hooks = json.loads(raw) if raw else {}
|
|
73
|
+
|
|
74
|
+
# Load current settings (tolerate corrupted JSON)
|
|
75
|
+
try:
|
|
76
|
+
with open(sys.argv[1]) as f:
|
|
77
|
+
current = json.load(f)
|
|
78
|
+
except Exception:
|
|
79
|
+
backup = sys.argv[1] + '.bak'
|
|
80
|
+
if os.path.isfile(sys.argv[1]):
|
|
81
|
+
shutil.copy2(sys.argv[1], backup)
|
|
82
|
+
print(f' Warning: settings.json corrupted — backed up to {os.path.basename(backup)}', file=sys.stderr)
|
|
83
|
+
current = {}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(sys.argv[2]) as f:
|
|
87
|
+
source = json.load(f)
|
|
88
|
+
except Exception:
|
|
89
|
+
source = {}
|
|
90
|
+
|
|
91
|
+
changes = []
|
|
92
|
+
|
|
93
|
+
# Merge hooks (regenerate from compose.yml)
|
|
94
|
+
if hooks and hooks != {}:
|
|
95
|
+
current['hooks'] = hooks
|
|
96
|
+
changes.append('hooks regenerated')
|
|
97
|
+
|
|
98
|
+
# Merge permissions (union: keep user additions + add new from source)
|
|
99
|
+
src_perms = source.get('permissions', {})
|
|
100
|
+
cur_perms = current.get('permissions', {})
|
|
101
|
+
for key in ('allow', 'deny'):
|
|
102
|
+
src_list = src_perms.get(key, [])
|
|
103
|
+
cur_list = cur_perms.get(key, [])
|
|
104
|
+
added = [p for p in src_list if p not in cur_list]
|
|
105
|
+
if added:
|
|
106
|
+
cur_list.extend(added)
|
|
107
|
+
changes.append(f'{len(added)} new {key} permission(s)')
|
|
108
|
+
cur_perms[key] = cur_list
|
|
109
|
+
current['permissions'] = cur_perms
|
|
110
|
+
|
|
111
|
+
with open(sys.argv[1], 'w') as f:
|
|
112
|
+
json.dump(current, f, indent=2)
|
|
113
|
+
f.write('\n')
|
|
114
|
+
|
|
115
|
+
if changes:
|
|
116
|
+
print(' Merged settings.json: ' + ', '.join(changes))
|
|
117
|
+
else:
|
|
118
|
+
print(' settings.json up to date — no merge needed')
|
|
119
|
+
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json" <<< "${hooks_json:-}"
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
PROJECT_ROOT="$(dirname "$TARGET")"
|
|
124
|
+
if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
|
|
125
|
+
cp "$SCRIPT_DIR/CLAUDE.md" "$PROJECT_ROOT/CLAUDE.md"
|
|
126
|
+
else
|
|
127
|
+
echo "CLAUDE.md already exists — skipping"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Git workflow config (interactive in --init, defaults otherwise)
|
|
131
|
+
if [ "$GLOBAL" = false ]; then
|
|
132
|
+
if [ "$INIT_MODE" = true ]; then
|
|
133
|
+
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
|
|
134
|
+
elif [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
135
|
+
# Apply sensible defaults without asking
|
|
136
|
+
GIT_BRANCHING="direct-main"
|
|
137
|
+
GIT_COMMIT_CONVENTION="conventional"
|
|
138
|
+
GIT_PR_PREFERENCE="on-demand"
|
|
139
|
+
GIT_AUTO_PUSH="after-commit"
|
|
140
|
+
GIT_WORK_BRANCH=""
|
|
141
|
+
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
|
|
146
|
+
|
|
147
|
+
# Write manifest for future updates
|
|
148
|
+
write_manifest "$TARGET" "$PROFILE" "claude" false
|
|
149
|
+
|
|
150
|
+
# Create docs/ workspace for agent outputs (local only)
|
|
151
|
+
if [ "$GLOBAL" = false ]; then
|
|
152
|
+
create_docs_workspace "$(dirname "$TARGET")"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
run_audit "$(dirname "$TARGET")"
|
|
156
|
+
update_gitignore "$(dirname "$TARGET")" ".claude"
|
|
157
|
+
setup_templates "$(dirname "$TARGET")"
|
|
158
|
+
|
|
159
|
+
# Offer native git post-merge hook (opt-in, local only, interactive only)
|
|
160
|
+
if [ "$GLOBAL" = false ] && [ -d "$(pwd)/.git" ]; then
|
|
161
|
+
if [ -t 0 ]; then
|
|
162
|
+
echo ""
|
|
163
|
+
read -r -p "Install native git post-merge hook (auto-cleanup after merges)? [y/N]: " _hook_confirm
|
|
164
|
+
case "$_hook_confirm" in
|
|
165
|
+
[yY]*) install_git_post_merge_hook "$(pwd)" ;;
|
|
166
|
+
*) echo " → post-merge hook skipped (run: cortexhawk install --post-merge-hook to add later)" ;;
|
|
167
|
+
esac
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
echo ""
|
|
172
|
+
echo "CortexHawk installed successfully for Claude Code!"
|
|
173
|
+
echo ""
|
|
174
|
+
echo " 35 commands | 20 agents | 36 skills | 11 hooks | 7 modes"
|
|
175
|
+
echo ""
|
|
176
|
+
do_quickstart
|
|
177
|
+
echo ""
|
|
178
|
+
echo " To activate: exit Claude Code (ctrl+c) and relaunch 'claude' in this directory."
|
|
179
|
+
}
|
|
@@ -93,7 +93,7 @@ case "$skill_choice" in
|
|
|
93
93
|
fi
|
|
94
94
|
|
|
95
95
|
# Generate custom profile JSON
|
|
96
|
-
PROFILE_FILE
|
|
96
|
+
PROFILE_FILE=$(mktemp /tmp/cortexhawk-custom-XXXXXX)
|
|
97
97
|
SKILL_LINES=""
|
|
98
98
|
SKILL_COUNT=0
|
|
99
99
|
for category in $SELECTED_CATS; do
|
|
@@ -133,7 +133,8 @@ if [ -n "$SKILLSMP_KEY" ]; then
|
|
|
133
133
|
else
|
|
134
134
|
echo "SKILLSMP_API_KEY=$SKILLSMP_KEY" >> .env
|
|
135
135
|
fi
|
|
136
|
-
|
|
136
|
+
chmod 600 .env
|
|
137
|
+
green " → saved to .env (permissions: owner-only)"
|
|
137
138
|
else
|
|
138
139
|
yellow " → skipped (use --search with local REGISTRY.md only)"
|
|
139
140
|
fi
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# lint-guard-runner.sh — Formatter/linter execution with detection cache + parallel linters
|
|
3
|
+
# Called by hooks/lint-guard.sh
|
|
4
|
+
# Formatters: sequential (git add must not race). Linters: parallel (check-only).
|
|
5
|
+
|
|
6
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
7
|
+
[ -z "$REPO_ROOT" ] && exit 0
|
|
8
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
9
|
+
[ -z "$STAGED" ] && exit 0
|
|
10
|
+
|
|
11
|
+
# Opt-out config
|
|
12
|
+
CONF_FILE="$REPO_ROOT/.claude/git-workflow.conf"
|
|
13
|
+
LINT_SKIP=$(grep '^LINT_SKIP=' "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr ',' '\n')
|
|
14
|
+
|
|
15
|
+
lint_cfg() {
|
|
16
|
+
local key="$1" default="${2:-auto}"
|
|
17
|
+
if [ -f "$REPO_ROOT/.cortexhawk-lint.yml" ]; then
|
|
18
|
+
local val
|
|
19
|
+
val=$(grep -E "^\s+$key:" "$REPO_ROOT/.cortexhawk-lint.yml" 2>/dev/null \
|
|
20
|
+
| sed 's/.*: *//' | tr -d ' \r')
|
|
21
|
+
echo "${val:-$default}"
|
|
22
|
+
else
|
|
23
|
+
echo "$default"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TIMEOUT_SECS=$(lint_cfg "timeout" "30")
|
|
28
|
+
case "$TIMEOUT_SECS" in ''|*[!0-9]*|0) TIMEOUT_SECS=30 ;; esac
|
|
29
|
+
FAIL_ON_FMT=$(lint_cfg "fail_on_formatter" "false")
|
|
30
|
+
TIMEOUT_CMD=""
|
|
31
|
+
command -v timeout &>/dev/null && TIMEOUT_CMD="timeout $TIMEOUT_SECS"
|
|
32
|
+
command -v gtimeout &>/dev/null && [ -z "$TIMEOUT_CMD" ] && TIMEOUT_CMD="gtimeout $TIMEOUT_SECS"
|
|
33
|
+
|
|
34
|
+
# --- DETECTION CACHE (1hr TTL, safe key=value, no source) ---
|
|
35
|
+
mkdir -p "$REPO_ROOT/.claude" 2>/dev/null || true
|
|
36
|
+
CACHE="$REPO_ROOT/.claude/lint-guard-cache"
|
|
37
|
+
_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || date +%s; }
|
|
38
|
+
[ -f "$CACHE" ] && [ $(( $(date +%s) - $(_mtime "$CACHE") )) -gt 3600 ] && rm -f "$CACHE"
|
|
39
|
+
cache_get() { grep -m1 "^$1=" "$CACHE" 2>/dev/null | cut -d= -f2; }
|
|
40
|
+
cache_set() { local _tmp; _tmp=$(mktemp "${CACHE}.XXXXXX"); { grep -v "^$1=" "$CACHE" 2>/dev/null; echo "$1=$2"; } > "$_tmp" && mv "$_tmp" "$CACHE" || rm -f "$_tmp"; }
|
|
41
|
+
|
|
42
|
+
# --- TOOL DETECTION ---
|
|
43
|
+
STAGED_JS=$(echo "$STAGED" | grep -E '\.(js|ts|jsx|tsx|mjs|cjs)$' || true)
|
|
44
|
+
STAGED_CSS=$(echo "$STAGED" | grep -E '\.(css|scss|sass|less)$' || true)
|
|
45
|
+
STAGED_PY=$(echo "$STAGED" | grep -E '\.py$' || true)
|
|
46
|
+
STAGED_RS=$(echo "$STAGED" | grep -E '\.rs$' || true)
|
|
47
|
+
STAGED_GO=$(echo "$STAGED" | grep -E '\.go$' || true)
|
|
48
|
+
|
|
49
|
+
has_config() {
|
|
50
|
+
case "$1" in
|
|
51
|
+
prettier) ls "$REPO_ROOT"/.prettierrc* "$REPO_ROOT"/prettier.config.* 2>/dev/null | grep -q . \
|
|
52
|
+
|| grep -qs '"prettier"' "$REPO_ROOT/package.json" 2>/dev/null ;;
|
|
53
|
+
eslint) ls "$REPO_ROOT"/.eslintrc* "$REPO_ROOT"/eslint.config.* 2>/dev/null | grep -q . ;;
|
|
54
|
+
black) grep -qs '\[tool\.black\]' "$REPO_ROOT/pyproject.toml" 2>/dev/null ;;
|
|
55
|
+
flake8) [ -f "$REPO_ROOT/.flake8" ] || grep -qs '\[flake8\]' "$REPO_ROOT/setup.cfg" 2>/dev/null ;;
|
|
56
|
+
mypy) grep -qs '\[tool\.mypy\]' "$REPO_ROOT/pyproject.toml" 2>/dev/null || [ -f "$REPO_ROOT/mypy.ini" ] ;;
|
|
57
|
+
stylelint) ls "$REPO_ROOT"/.stylelintrc* "$REPO_ROOT"/stylelint.config.* 2>/dev/null | grep -q . ;;
|
|
58
|
+
rustfmt) [ -f "$REPO_ROOT/rustfmt.toml" ] || [ -f "$REPO_ROOT/.rustfmt.toml" ] ;;
|
|
59
|
+
gofmt) true ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
c_has_config() {
|
|
64
|
+
local key="HAS_$(echo "$1" | tr '[:lower:]' '[:upper:]')"
|
|
65
|
+
local cached; cached=$(cache_get "$key")
|
|
66
|
+
if [ -n "$cached" ]; then [ "$cached" = "1" ]; return $?; fi
|
|
67
|
+
if has_config "$1"; then cache_set "$key" "1"; return 0
|
|
68
|
+
else cache_set "$key" "0"; return 1; fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
is_enabled() {
|
|
72
|
+
echo "$LINT_SKIP" | grep -qx "$1" && return 1
|
|
73
|
+
local val; val=$(lint_cfg "$1"); [ "$val" != "false" ]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tool_ok() { command -v "$1" &>/dev/null; }
|
|
77
|
+
|
|
78
|
+
# Pre-populate cache before parallel stage to avoid write races
|
|
79
|
+
for _t in prettier eslint black flake8 mypy stylelint rustfmt gofmt; do
|
|
80
|
+
c_has_config "$_t" >/dev/null 2>&1
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
# --- FORMATTERS (sequential — git add must not race) ---
|
|
84
|
+
run_formatter() {
|
|
85
|
+
local name="$1" cmd="$2" files="$3"
|
|
86
|
+
is_enabled "$name" || return 0
|
|
87
|
+
c_has_config "$name" || return 0
|
|
88
|
+
tool_ok "$name" || { echo "lint-guard: $name not in PATH, skipping"; return 0; }
|
|
89
|
+
[ -z "$files" ] && return 0
|
|
90
|
+
echo "lint-guard: $name (auto-fix)..."
|
|
91
|
+
# shellcheck disable=SC2086
|
|
92
|
+
if echo "$files" | tr '\n' '\0' | xargs -0 $TIMEOUT_CMD $cmd 2>/dev/null; then
|
|
93
|
+
echo "$files" | tr '\n' '\0' | xargs -0 git add 2>/dev/null || true
|
|
94
|
+
else
|
|
95
|
+
echo "lint-guard: $name formatter failed" >&2
|
|
96
|
+
[ "$FAIL_ON_FMT" = "true" ] && exit 2
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[ -n "$STAGED_JS" ] && run_formatter "prettier" "prettier --write" "$STAGED_JS"
|
|
101
|
+
[ -n "$STAGED_CSS" ] && run_formatter "stylelint" "stylelint --fix" "$STAGED_CSS"
|
|
102
|
+
[ -n "$STAGED_PY" ] && run_formatter "black" "black" "$STAGED_PY"
|
|
103
|
+
[ -n "$STAGED_RS" ] && run_formatter "rustfmt" "rustfmt" "$STAGED_RS"
|
|
104
|
+
[ -n "$STAGED_GO" ] && run_formatter "gofmt" "gofmt -w" "$STAGED_GO"
|
|
105
|
+
|
|
106
|
+
# --- LINTERS (parallel — check-only, no file writes) ---
|
|
107
|
+
ERR_DIR=$(mktemp -d)
|
|
108
|
+
trap 'rm -rf "$ERR_DIR"' EXIT
|
|
109
|
+
|
|
110
|
+
run_linter_bg() {
|
|
111
|
+
local name="$1" cmd="$2" files="$3"
|
|
112
|
+
is_enabled "$name" || return 0
|
|
113
|
+
c_has_config "$name" || return 0
|
|
114
|
+
tool_ok "$name" || { echo "lint-guard: $name not in PATH, skipping"; return 0; }
|
|
115
|
+
[ -z "$files" ] && return 0
|
|
116
|
+
echo "lint-guard: $name (check)..."
|
|
117
|
+
# shellcheck disable=SC2086
|
|
118
|
+
if ! echo "$files" | tr '\n' '\0' | xargs -0 $TIMEOUT_CMD $cmd 2>&1; then
|
|
119
|
+
echo "BLOCKED by lint-guard: $name errors found. Fix above, re-stage, and retry." >&2
|
|
120
|
+
touch "$ERR_DIR/$name"
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Output from parallel linters may interleave on screen; ERR_DIR signals are reliable regardless.
|
|
125
|
+
[ -n "$STAGED_JS" ] && run_linter_bg "eslint" "eslint --max-warnings=0" "$STAGED_JS" &
|
|
126
|
+
[ -n "$STAGED_PY" ] && run_linter_bg "flake8" "flake8" "$STAGED_PY" &
|
|
127
|
+
[ -n "$STAGED_PY" ] && run_linter_bg "mypy" "mypy --no-error-summary" "$STAGED_PY" &
|
|
128
|
+
wait
|
|
129
|
+
|
|
130
|
+
ERRORS=$(find "$ERR_DIR" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
131
|
+
[ "$ERRORS" -gt 0 ] && exit 2
|
|
132
|
+
exit 0
|