@northbridge-security/secureai 0.1.13
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/README.md +122 -0
- package/.claude/commands/architect/clean.md +978 -0
- package/.claude/commands/architect/kiss.md +762 -0
- package/.claude/commands/architect/review.md +704 -0
- package/.claude/commands/catchup.md +90 -0
- package/.claude/commands/code.md +115 -0
- package/.claude/commands/commit.md +1218 -0
- package/.claude/commands/cover.md +1298 -0
- package/.claude/commands/fmea.md +275 -0
- package/.claude/commands/kaizen.md +312 -0
- package/.claude/commands/pr.md +503 -0
- package/.claude/commands/todo.md +99 -0
- package/.claude/commands/worktree.md +738 -0
- package/.claude/commands/wrapup.md +103 -0
- package/LICENSE +183 -0
- package/README.md +108 -0
- package/dist/cli.js +75634 -0
- package/docs/agents/devops-reviewer.md +889 -0
- package/docs/agents/kiss-simplifier.md +1088 -0
- package/docs/agents/typescript.md +8 -0
- package/docs/guides/README.md +109 -0
- package/docs/guides/agents.clean.arch.md +244 -0
- package/docs/guides/agents.clean.arch.ts.md +1314 -0
- package/docs/guides/agents.gotask.md +1037 -0
- package/docs/guides/agents.markdown.md +1209 -0
- package/docs/guides/agents.onepassword.md +285 -0
- package/docs/guides/agents.sonar.md +857 -0
- package/docs/guides/agents.tdd.md +838 -0
- package/docs/guides/agents.tdd.ts.md +1062 -0
- package/docs/guides/agents.typesript.md +1389 -0
- package/docs/guides/github-mcp.md +1075 -0
- package/package.json +130 -0
- package/packages/secureai-cli/src/cli.ts +21 -0
- package/tasks/README.md +880 -0
- package/tasks/aws.yml +64 -0
- package/tasks/bash.yml +118 -0
- package/tasks/bun.yml +738 -0
- package/tasks/claude.yml +183 -0
- package/tasks/docker.yml +420 -0
- package/tasks/docs.yml +127 -0
- package/tasks/git.yml +1336 -0
- package/tasks/gotask.yml +132 -0
- package/tasks/json.yml +77 -0
- package/tasks/markdown.yml +95 -0
- package/tasks/onepassword.yml +350 -0
- package/tasks/security.yml +102 -0
- package/tasks/sonar.yml +437 -0
- package/tasks/template.yml +74 -0
- package/tasks/vscode.yml +103 -0
- package/tasks/yaml.yml +121 -0
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Analyze changes, generate commit messages, run QA checks, and create grouped commits
|
|
3
|
+
argument-hint: [files...] [--amend] [--all] [--message="text"] [--no-verify] [--dry-run] [--compare] [--quick]
|
|
4
|
+
allowed-tools: Bash, Read, Edit, Write, Grep, Bash(git *), Bash(task *), mcp__task_master_ai__*
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Commit Command: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
Intelligently groups related files and creates multiple focused commits with generated conventional commit messages, comprehensive validation, and strict hook compliance.
|
|
10
|
+
|
|
11
|
+
**IMPORTANT - Report Location Pattern**: This command generates a commit report. See [REPORT-LOCATIONS.md](./REPORT-LOCATIONS.md) for the required pattern:
|
|
12
|
+
|
|
13
|
+
- **Save to temporary directory**: `${TMPDIR:-${TEMP:-/tmp}}/commit-report-$(date +%Y%m%d-%H%M%S).md`
|
|
14
|
+
- **Auto-open in VSCode**: `code "$REPORT_FILE"` after generation
|
|
15
|
+
- **Never save to project root**: No `.commit.report.local.md` files
|
|
16
|
+
|
|
17
|
+
**Key Features:**
|
|
18
|
+
|
|
19
|
+
- **Smart grouping** - Groups files by Task Master context, bug tracking, and logical relationships
|
|
20
|
+
- **Multiple commits** - Creates separate commits for unrelated changes
|
|
21
|
+
- **Task context** - Uses Task Master to understand what work files belong to (task IDs stay internal)
|
|
22
|
+
- **Strict validation** - Respects all commit hooks, validates file readiness
|
|
23
|
+
- **Comprehensive reporting** - Detailed report in `.commit.report.local.md`
|
|
24
|
+
|
|
25
|
+
## Quick Examples
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Commit all staged files (auto-grouped into multiple commits)
|
|
29
|
+
/commit
|
|
30
|
+
|
|
31
|
+
# Commit specific files
|
|
32
|
+
/commit src/cli/*.ts tests/cli/*.test.ts
|
|
33
|
+
|
|
34
|
+
# Stage and commit everything (auto-grouped)
|
|
35
|
+
/commit --all
|
|
36
|
+
|
|
37
|
+
# Preview what would be committed
|
|
38
|
+
/commit --dry-run --all
|
|
39
|
+
|
|
40
|
+
# Use custom message for single commit (bypasses grouping)
|
|
41
|
+
/commit --message="chore: update dependencies" package.json bun.lock
|
|
42
|
+
|
|
43
|
+
# Amend previous commit
|
|
44
|
+
/commit --amend src/fix.ts
|
|
45
|
+
|
|
46
|
+
# Compare with previous attempt
|
|
47
|
+
/commit --all --compare
|
|
48
|
+
|
|
49
|
+
# Quick mode - commit files from current conversation
|
|
50
|
+
/commit --quick
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Phase 0: Parse Arguments
|
|
54
|
+
|
|
55
|
+
Extract from `$ARGUMENTS`:
|
|
56
|
+
|
|
57
|
+
- **Files**: Optional file list (e.g., `src/cli/*.ts Taskfile.yml`)
|
|
58
|
+
- **--all**: Stage all modified files before committing
|
|
59
|
+
- **--amend**: Amend the previous commit instead of creating new
|
|
60
|
+
- **--message="text"**: Use provided message (creates single commit, bypasses grouping)
|
|
61
|
+
- **--no-verify**: Skip git hooks (dangerous, requires explicit confirmation)
|
|
62
|
+
- **--dry-run**: Show what would be committed without actually committing
|
|
63
|
+
- **--compare**: Compare with previous commit attempt (if `.commit.report.local.md` exists)
|
|
64
|
+
- **--quick**: Quick mode - commit files discussed in current conversation with minimal ceremony
|
|
65
|
+
|
|
66
|
+
**If `--quick` flag is present, skip to [Quick Mode](#quick-mode) section.**
|
|
67
|
+
|
|
68
|
+
## Quick Mode
|
|
69
|
+
|
|
70
|
+
Quick mode is designed for committing files discussed in the current conversation with minimal ceremony. This is useful for smaller tool changes or work done outside the main task context.
|
|
71
|
+
|
|
72
|
+
**When to use `--quick`:**
|
|
73
|
+
|
|
74
|
+
- Small configuration or tool changes
|
|
75
|
+
- Files edited during a side conversation
|
|
76
|
+
- Quick fixes unrelated to current Task Master tasks
|
|
77
|
+
- Documentation updates from chat discussion
|
|
78
|
+
|
|
79
|
+
**What Quick Mode does:**
|
|
80
|
+
|
|
81
|
+
1. Extracts files discussed/edited in current conversation
|
|
82
|
+
2. Creates a single focused commit
|
|
83
|
+
3. Generates conventional commit message from conversation context
|
|
84
|
+
4. Respects all git hooks (no `--no-verify`)
|
|
85
|
+
5. Skips complex grouping logic
|
|
86
|
+
|
|
87
|
+
### Quick Mode Workflow
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// Step 1: Extract files from conversation context
|
|
91
|
+
// Look for files that were:
|
|
92
|
+
// - Read with Read tool
|
|
93
|
+
// - Edited with Edit tool
|
|
94
|
+
// - Written with Write tool
|
|
95
|
+
// - Discussed explicitly by path
|
|
96
|
+
|
|
97
|
+
const conversationFiles = extractFilesFromConversation();
|
|
98
|
+
// Example result: ['docs/commands/commit.md', '.claude/settings.json']
|
|
99
|
+
|
|
100
|
+
// Step 2: Filter to only modified files
|
|
101
|
+
const modifiedFiles = conversationFiles.filter((file) => {
|
|
102
|
+
// Check git status - only include files with actual changes
|
|
103
|
+
const status = execSync(`git status --porcelain "${file}"`).toString().trim();
|
|
104
|
+
return status.length > 0;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Step 3: Validate files exist and are trackable
|
|
108
|
+
const validFiles = modifiedFiles.filter((file) => {
|
|
109
|
+
return existsSync(file) && !isIgnored(file);
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Quick Mode: Stage and Validate
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Stage only the conversation files
|
|
117
|
+
for file in "${QUICK_FILES[@]}"; do
|
|
118
|
+
if [ -f "$file" ]; then
|
|
119
|
+
git add "$file"
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
|
|
123
|
+
# Verify we have files to commit
|
|
124
|
+
STAGED=$(git diff --cached --name-only)
|
|
125
|
+
if [ -z "$STAGED" ]; then
|
|
126
|
+
echo "❌ No modified files from conversation to commit"
|
|
127
|
+
echo " Files checked: ${QUICK_FILES[*]}"
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
echo "📝 Quick commit: $(echo "$STAGED" | wc -l | tr -d ' ') files"
|
|
132
|
+
echo "$STAGED" | while read -r file; do
|
|
133
|
+
echo " - $file"
|
|
134
|
+
done
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Quick Mode: Generate Commit Message
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Analyze conversation context to determine commit type and scope
|
|
141
|
+
|
|
142
|
+
// Extract the main topic from conversation
|
|
143
|
+
const topic = analyzeConversationTopic();
|
|
144
|
+
// Example: "updating commit command with quick flag"
|
|
145
|
+
|
|
146
|
+
// Determine commit type from changes
|
|
147
|
+
let commitType = "chore";
|
|
148
|
+
const fileTypes = validFiles.map((f) => getFileType(f));
|
|
149
|
+
|
|
150
|
+
if (fileTypes.some((t) => t === "feature-code")) {
|
|
151
|
+
commitType = "feat";
|
|
152
|
+
} else if (fileTypes.some((t) => t === "test")) {
|
|
153
|
+
commitType = "test";
|
|
154
|
+
} else if (fileTypes.every((t) => t === "docs")) {
|
|
155
|
+
commitType = "docs";
|
|
156
|
+
} else if (fileTypes.some((t) => t === "config")) {
|
|
157
|
+
commitType = "chore";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Determine scope from file paths
|
|
161
|
+
const scope = inferScopeFromPaths(validFiles);
|
|
162
|
+
// Example: 'commands' from 'docs/commands/commit.md'
|
|
163
|
+
|
|
164
|
+
// Generate description from conversation context
|
|
165
|
+
const description = generateDescriptionFromContext(topic);
|
|
166
|
+
// Example: 'add quick flag for conversation-based commits'
|
|
167
|
+
|
|
168
|
+
// Compose message
|
|
169
|
+
const message = scope ? `${commitType}(${scope}): ${description}` : `${commitType}: ${description}`;
|
|
170
|
+
|
|
171
|
+
// Example: "docs(commands): add quick flag for conversation-based commits"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Quick Mode: Execute Commit
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Run pre-commit hooks (NEVER skip in quick mode)
|
|
178
|
+
echo "🔍 Running pre-commit checks..."
|
|
179
|
+
|
|
180
|
+
# Create commit with generated message
|
|
181
|
+
if git commit -m "$(cat <<'EOF'
|
|
182
|
+
$COMMIT_MESSAGE
|
|
183
|
+
EOF
|
|
184
|
+
)"; then
|
|
185
|
+
COMMIT_SHA=$(git rev-parse HEAD)
|
|
186
|
+
echo ""
|
|
187
|
+
echo "✅ Quick commit successful"
|
|
188
|
+
echo " SHA: ${COMMIT_SHA:0:7}"
|
|
189
|
+
echo " Message: $COMMIT_MESSAGE"
|
|
190
|
+
echo ""
|
|
191
|
+
|
|
192
|
+
# Show files committed
|
|
193
|
+
echo "📁 Files committed:"
|
|
194
|
+
git diff-tree --no-commit-id --name-only -r HEAD | while read -r file; do
|
|
195
|
+
echo " - $file"
|
|
196
|
+
done
|
|
197
|
+
else
|
|
198
|
+
echo "❌ Commit failed"
|
|
199
|
+
echo ""
|
|
200
|
+
echo "Possible causes:"
|
|
201
|
+
echo " - Pre-commit hooks failed (fix lint/type errors)"
|
|
202
|
+
echo " - Commit message format rejected"
|
|
203
|
+
echo ""
|
|
204
|
+
echo "Fix issues and retry with: /commit --quick"
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Quick Mode: File Extraction Patterns
|
|
210
|
+
|
|
211
|
+
**Files to include from conversation:**
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// Pattern 1: Files read with Read tool
|
|
215
|
+
// Look for: Read tool calls in conversation
|
|
216
|
+
const readFiles = conversation.toolCalls
|
|
217
|
+
.filter((call) => call.tool === "Read")
|
|
218
|
+
.map((call) => call.params.file_path);
|
|
219
|
+
|
|
220
|
+
// Pattern 2: Files edited with Edit tool
|
|
221
|
+
// Look for: Edit tool calls in conversation
|
|
222
|
+
const editedFiles = conversation.toolCalls
|
|
223
|
+
.filter((call) => call.tool === "Edit")
|
|
224
|
+
.map((call) => call.params.file_path);
|
|
225
|
+
|
|
226
|
+
// Pattern 3: Files written with Write tool
|
|
227
|
+
const writtenFiles = conversation.toolCalls
|
|
228
|
+
.filter((call) => call.tool === "Write")
|
|
229
|
+
.map((call) => call.params.file_path);
|
|
230
|
+
|
|
231
|
+
// Pattern 4: Files explicitly mentioned by path
|
|
232
|
+
// Look for: file paths in user messages
|
|
233
|
+
const mentionedFiles = extractFilePathsFromMessages(conversation.messages);
|
|
234
|
+
|
|
235
|
+
// Combine and deduplicate
|
|
236
|
+
const allFiles = [...new Set([...readFiles, ...editedFiles, ...writtenFiles, ...mentionedFiles])];
|
|
237
|
+
|
|
238
|
+
// Filter to only include files with modifications
|
|
239
|
+
return allFiles.filter(hasUncommittedChanges);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Files to exclude:**
|
|
243
|
+
|
|
244
|
+
- Files in `.gitignore`
|
|
245
|
+
- Files with no actual changes (read-only access)
|
|
246
|
+
- Temporary files (`.tmp/*`, `*.log`)
|
|
247
|
+
- Files outside repository root
|
|
248
|
+
|
|
249
|
+
### Quick Mode vs Standard Mode
|
|
250
|
+
|
|
251
|
+
| Aspect | Quick Mode | Standard Mode |
|
|
252
|
+
| -------------- | ----------------- | ------------------------ |
|
|
253
|
+
| File selection | From conversation | All staged or specified |
|
|
254
|
+
| Grouping | Single commit | Multiple grouped commits |
|
|
255
|
+
| Task Master | Not used | Used for context |
|
|
256
|
+
| Complexity | Minimal | Full analysis |
|
|
257
|
+
| Use case | Small changes | Large changesets |
|
|
258
|
+
| Hooks | Always run | Always run |
|
|
259
|
+
|
|
260
|
+
**Important**: Quick mode still respects all git hooks and conventional commit format. The only simplification is in file selection and grouping logic.
|
|
261
|
+
|
|
262
|
+
## Phase 1: Analyze Git State
|
|
263
|
+
|
|
264
|
+
### 1.1 Verify Git Repository
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Check if we're in a git repository
|
|
268
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
|
269
|
+
echo "❌ Error: Not a git repository"
|
|
270
|
+
echo " Initialize with: git init"
|
|
271
|
+
exit 1
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Get current branch
|
|
275
|
+
BRANCH=$(git branch --show-current)
|
|
276
|
+
|
|
277
|
+
# Warn if on protected branch
|
|
278
|
+
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
|
|
279
|
+
echo "⚠️ Warning: Committing to protected branch '$BRANCH'"
|
|
280
|
+
echo " Consider using a feature branch instead"
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# Check for merge in progress
|
|
284
|
+
if [ -f .git/MERGE_HEAD ]; then
|
|
285
|
+
echo "❌ Error: Merge in progress"
|
|
286
|
+
echo " Resolve conflicts first with: git status"
|
|
287
|
+
exit 1
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# Check for detached HEAD
|
|
291
|
+
if ! git symbolic-ref -q HEAD >/dev/null; then
|
|
292
|
+
echo "⚠️ Warning: Detached HEAD state"
|
|
293
|
+
echo " Commits will not be on a branch"
|
|
294
|
+
echo " Consider: git checkout -b <branch-name>"
|
|
295
|
+
fi
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 1.2 Collect Changed Files
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Determine which files to process
|
|
302
|
+
if [ -n "$FILES_ARG" ]; then
|
|
303
|
+
# Specific files provided
|
|
304
|
+
CHANGED_FILES=("$FILES_ARG")
|
|
305
|
+
|
|
306
|
+
# Validate files exist
|
|
307
|
+
for file in "${CHANGED_FILES[@]}"; do
|
|
308
|
+
[ -f "$file" ] || {
|
|
309
|
+
echo "❌ Error: File not found: $file"
|
|
310
|
+
exit 1
|
|
311
|
+
}
|
|
312
|
+
done
|
|
313
|
+
|
|
314
|
+
# Stage specified files
|
|
315
|
+
git add "${CHANGED_FILES[@]}"
|
|
316
|
+
elif [ "$ALL_FLAG" = "true" ]; then
|
|
317
|
+
# Stage all modified and untracked files
|
|
318
|
+
git add -A
|
|
319
|
+
CHANGED_FILES=($(git diff --cached --name-only))
|
|
320
|
+
else
|
|
321
|
+
# Use currently staged files
|
|
322
|
+
CHANGED_FILES=($(git diff --cached --name-only))
|
|
323
|
+
|
|
324
|
+
# Warn if nothing is staged
|
|
325
|
+
if [ ${#CHANGED_FILES[@]} -eq 0 ]; then
|
|
326
|
+
echo "❌ Error: No files to commit"
|
|
327
|
+
echo " Options:"
|
|
328
|
+
echo " - Stage files: git add <file>"
|
|
329
|
+
echo " - Use --all: /commit --all"
|
|
330
|
+
exit 1
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
TOTAL_FILES=${#CHANGED_FILES[@]}
|
|
335
|
+
echo "📊 Analyzing $TOTAL_FILES changed files..."
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Phase 2: Validate Files
|
|
339
|
+
|
|
340
|
+
For each changed file, validate readiness:
|
|
341
|
+
|
|
342
|
+
### 2.1 Syntax and Completeness Validation
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
READY_FILES=()
|
|
346
|
+
SKIPPED_FILES=()
|
|
347
|
+
|
|
348
|
+
for file in "${CHANGED_FILES[@]}"; do
|
|
349
|
+
SKIP_REASON=""
|
|
350
|
+
|
|
351
|
+
# Empty files
|
|
352
|
+
if [ ! -s "$file" ] || ! grep -q '[^[:space:]]' "$file" 2>/dev/null; then
|
|
353
|
+
SKIP_REASON="Empty file or whitespace only"
|
|
354
|
+
|
|
355
|
+
# JSON syntax
|
|
356
|
+
elif [[ "$file" == *.json ]] && ! jq empty "$file" 2>/dev/null; then
|
|
357
|
+
SKIP_REASON="Invalid JSON syntax"
|
|
358
|
+
|
|
359
|
+
# YAML syntax
|
|
360
|
+
elif [[ "$file" =~ \.(yaml|yml)$ ]] && command -v yamllint &>/dev/null; then
|
|
361
|
+
if ! yamllint -d relaxed "$file" 2>/dev/null; then
|
|
362
|
+
SKIP_REASON="Invalid YAML syntax"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# TypeScript/JavaScript syntax
|
|
366
|
+
elif [[ "$file" =~ \.(ts|tsx|js|jsx)$ ]] && command -v node &>/dev/null; then
|
|
367
|
+
if ! node -c "$file" 2>/dev/null; then
|
|
368
|
+
SKIP_REASON="Syntax errors detected"
|
|
369
|
+
fi
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# WIP markers
|
|
373
|
+
if [ -z "$SKIP_REASON" ] && grep -qiE '\bWIP\b|TODO:.*finish|FIXME:.*broken|HACK:' "$file"; then
|
|
374
|
+
MARKERS=$(grep -niE '\bWIP\b|TODO:.*finish|FIXME:.*broken|HACK:' "$file" | head -3)
|
|
375
|
+
SKIP_REASON="Contains WIP markers: $MARKERS"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# Debug code
|
|
379
|
+
if [ -z "$SKIP_REASON" ] && grep -qE 'console\.log.*["\x27]test["\x27]|debugger;|console\.debug' "$file"; then
|
|
380
|
+
DEBUG_LINES=$(grep -nE 'console\.log.*["\x27]test["\x27]|debugger;|console\.debug' "$file" | head -3)
|
|
381
|
+
SKIP_REASON="Contains debug code: $DEBUG_LINES"
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# Add to appropriate list
|
|
385
|
+
if [ -n "$SKIP_REASON" ]; then
|
|
386
|
+
SKIPPED_FILES+=("$file:$SKIP_REASON")
|
|
387
|
+
git reset "$file" 2>/dev/null || true
|
|
388
|
+
else
|
|
389
|
+
READY_FILES+=("$file")
|
|
390
|
+
fi
|
|
391
|
+
done
|
|
392
|
+
|
|
393
|
+
echo "✅ Validated: ${#READY_FILES[@]} files ready, ${#SKIPPED_FILES[@]} skipped"
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 2.2 Sensitive Data Detection
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
SENSITIVE_FILES=()
|
|
400
|
+
|
|
401
|
+
for file in "${READY_FILES[@]}"; do
|
|
402
|
+
# API keys
|
|
403
|
+
if grep -qE 'api[_-]?key.*["\x27][a-zA-Z0-9]{32,}["\x27]' "$file"; then
|
|
404
|
+
SENSITIVE_FILES+=("$file:Potential API key detected")
|
|
405
|
+
|
|
406
|
+
# AWS credentials
|
|
407
|
+
elif grep -qE 'AKIA[0-9A-Z]{16}' "$file"; then
|
|
408
|
+
SENSITIVE_FILES+=("$file:AWS access key detected")
|
|
409
|
+
|
|
410
|
+
# Private keys
|
|
411
|
+
elif grep -q '-----BEGIN.*PRIVATE KEY-----' "$file"; then
|
|
412
|
+
SENSITIVE_FILES+=("$file:Private key detected")
|
|
413
|
+
|
|
414
|
+
# JWT tokens
|
|
415
|
+
elif grep -qE 'eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*' "$file"; then
|
|
416
|
+
SENSITIVE_FILES+=("$file:JWT token detected")
|
|
417
|
+
|
|
418
|
+
# Generic secrets
|
|
419
|
+
elif grep -qiE 'secret.*=.*["\x27][a-zA-Z0-9]{16,}["\x27]' "$file"; then
|
|
420
|
+
SENSITIVE_FILES+=("$file:Potential secret detected")
|
|
421
|
+
fi
|
|
422
|
+
done
|
|
423
|
+
|
|
424
|
+
# Block commit if sensitive data found
|
|
425
|
+
if [ ${#SENSITIVE_FILES[@]} -gt 0 ]; then
|
|
426
|
+
echo "❌ BLOCKED: Sensitive data detected:"
|
|
427
|
+
for item in "${SENSITIVE_FILES[@]}"; do
|
|
428
|
+
echo " - $item"
|
|
429
|
+
done
|
|
430
|
+
echo ""
|
|
431
|
+
echo "Remove sensitive data before committing"
|
|
432
|
+
exit 1
|
|
433
|
+
fi
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 2.3 Conflict and Size Detection
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
# Check for unresolved conflicts
|
|
440
|
+
CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
|
|
441
|
+
if [ -n "$CONFLICT_FILES" ]; then
|
|
442
|
+
echo "❌ BLOCKED: Unresolved merge conflicts in:"
|
|
443
|
+
echo "$CONFLICT_FILES" | while read -r file; do
|
|
444
|
+
echo " - $file"
|
|
445
|
+
grep -n '^<<<<<<< \|^=======$\|^>>>>>>> ' "$file" 2>/dev/null | head -3 || true
|
|
446
|
+
done
|
|
447
|
+
echo ""
|
|
448
|
+
echo "Resolve conflicts before committing"
|
|
449
|
+
exit 1
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# Warn about large files
|
|
453
|
+
LARGE_FILES=()
|
|
454
|
+
for file in "${READY_FILES[@]}"; do
|
|
455
|
+
if [ -f "$file" ]; then
|
|
456
|
+
SIZE=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
|
|
457
|
+
if [ "$SIZE" -gt 1048576 ]; then # 1MB
|
|
458
|
+
SIZE_MB=$(echo "scale=2; $SIZE / 1048576" | bc)
|
|
459
|
+
LARGE_FILES+=("$file:${SIZE_MB}MB")
|
|
460
|
+
fi
|
|
461
|
+
fi
|
|
462
|
+
done
|
|
463
|
+
|
|
464
|
+
if [ ${#LARGE_FILES[@]} -gt 0 ]; then
|
|
465
|
+
echo "⚠️ Large files detected (>1MB):"
|
|
466
|
+
for item in "${LARGE_FILES[@]}"; do
|
|
467
|
+
echo " - $item"
|
|
468
|
+
done
|
|
469
|
+
echo " Consider using Git LFS for large binary files"
|
|
470
|
+
echo ""
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# Detect partial staging
|
|
474
|
+
PARTIAL_STAGED=$(comm -12 \
|
|
475
|
+
<(git diff --name-only | sort) \
|
|
476
|
+
<(git diff --cached --name-only | sort))
|
|
477
|
+
|
|
478
|
+
if [ -n "$PARTIAL_STAGED" ]; then
|
|
479
|
+
echo "⚠️ Files with partial staging (staged + unstaged changes):"
|
|
480
|
+
echo "$PARTIAL_STAGED" | while read -r file; do
|
|
481
|
+
echo " - $file"
|
|
482
|
+
done
|
|
483
|
+
echo " Only staged changes will be committed"
|
|
484
|
+
echo ""
|
|
485
|
+
fi
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Phase 3: Group Files by Context
|
|
489
|
+
|
|
490
|
+
**CRITICAL**: Never commit 40+ files in one commit. Group changes by function and create multiple focused commits.
|
|
491
|
+
|
|
492
|
+
### 3.1 Load Task Master Context
|
|
493
|
+
|
|
494
|
+
**IMPORTANT**: Task IDs are internal to the operator and MUST NOT appear in commit messages. Use Task Master only to understand what work is being done and group files accordingly.
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
# Check if Task Master is available
|
|
498
|
+
TASKMASTER_ENABLED=false
|
|
499
|
+
if [ -d .taskmaster ]; then
|
|
500
|
+
TASKMASTER_ENABLED=true
|
|
501
|
+
echo "🔍 Loading Task Master context..."
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
# Initialize arrays for grouping
|
|
505
|
+
declare -A TASK_CONTEXTS
|
|
506
|
+
declare -A FILE_TO_GROUP
|
|
507
|
+
|
|
508
|
+
if [ "$TASKMASTER_ENABLED" = "true" ]; then
|
|
509
|
+
# Get recent and in-progress tasks using MCP
|
|
510
|
+
# Focus on: status in (in-progress, done) AND updated within last 7 days
|
|
511
|
+
|
|
512
|
+
# For each task, collect:
|
|
513
|
+
# - Title (for commit description)
|
|
514
|
+
# - Description (for context matching)
|
|
515
|
+
# - Details (may contain file paths, function names)
|
|
516
|
+
# - Affected files (from git branch or task notes)
|
|
517
|
+
|
|
518
|
+
# Example pseudo-code for MCP call:
|
|
519
|
+
# TASKS=$(call mcp__task_master_ai__get_tasks with status filter)
|
|
520
|
+
# For each TASK:
|
|
521
|
+
# TASK_CONTEXTS[task_key]="title|description|scope"
|
|
522
|
+
# Store task context WITHOUT storing task ID in commit messages
|
|
523
|
+
fi
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 3.2 Analyze Files and Match to Contexts
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# For each ready file, determine which group it belongs to
|
|
530
|
+
for file in "${READY_FILES[@]}"; do
|
|
531
|
+
FILE_DIR=$(dirname "$file")
|
|
532
|
+
FILE_NAME=$(basename "$file")
|
|
533
|
+
FILE_EXT="${FILE_NAME##*.}"
|
|
534
|
+
|
|
535
|
+
# Extract file diff to understand changes
|
|
536
|
+
FILE_DIFF=$(git diff --cached --unified=3 "$file" | grep -E '^\+' | head -20)
|
|
537
|
+
|
|
538
|
+
# Match to Task Master context (if available)
|
|
539
|
+
MATCHED_GROUP=""
|
|
540
|
+
if [ "$TASKMASTER_ENABLED" = "true" ]; then
|
|
541
|
+
# For each task context:
|
|
542
|
+
for task_key in "${!TASK_CONTEXTS[@]}"; do
|
|
543
|
+
CONTEXT="${TASK_CONTEXTS[$task_key]}"
|
|
544
|
+
TITLE=$(echo "$CONTEXT" | cut -d'|' -f1)
|
|
545
|
+
DESCRIPTION=$(echo "$CONTEXT" | cut -d'|' -f2)
|
|
546
|
+
SCOPE=$(echo "$CONTEXT" | cut -d'|' -f3)
|
|
547
|
+
|
|
548
|
+
# Match file to task by:
|
|
549
|
+
# 1. File path mentioned in task details
|
|
550
|
+
# 2. Directory matches task scope (e.g., task about "cli" → src/cli/*.ts)
|
|
551
|
+
# 3. Keywords from task title match file path or diff
|
|
552
|
+
# 4. Related files (if test changed, implementation likely related to same task)
|
|
553
|
+
|
|
554
|
+
# Fuzzy matching logic:
|
|
555
|
+
MATCH_SCORE=0
|
|
556
|
+
|
|
557
|
+
# Check if file path mentioned in description
|
|
558
|
+
if echo "$DESCRIPTION" | grep -q "$file"; then
|
|
559
|
+
MATCH_SCORE=$((MATCH_SCORE + 10))
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
# Check if directory matches scope
|
|
563
|
+
if [[ "$FILE_DIR" =~ $SCOPE ]]; then
|
|
564
|
+
MATCH_SCORE=$((MATCH_SCORE + 5))
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
# Check keyword overlap
|
|
568
|
+
TITLE_KEYWORDS=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr -s '[:space:]' '\n')
|
|
569
|
+
for keyword in $TITLE_KEYWORDS; do
|
|
570
|
+
if [[ "$file" =~ $keyword ]] || echo "$FILE_DIFF" | grep -qi "$keyword"; then
|
|
571
|
+
MATCH_SCORE=$((MATCH_SCORE + 2))
|
|
572
|
+
fi
|
|
573
|
+
done
|
|
574
|
+
|
|
575
|
+
# Assign to best matching task context
|
|
576
|
+
if [ $MATCH_SCORE -gt 5 ]; then
|
|
577
|
+
MATCHED_GROUP="task:$task_key"
|
|
578
|
+
break
|
|
579
|
+
fi
|
|
580
|
+
done
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
# If no Task Master match, use logical grouping
|
|
584
|
+
if [ -z "$MATCHED_GROUP" ]; then
|
|
585
|
+
case "$file" in
|
|
586
|
+
tests/*test.ts)
|
|
587
|
+
# Find matching implementation
|
|
588
|
+
IMPL_FILE=$(echo "$file" | sed 's|tests/||; s|\.test\.ts$|.ts|')
|
|
589
|
+
if echo "${READY_FILES[@]}" | grep -q "$IMPL_FILE"; then
|
|
590
|
+
MATCHED_GROUP="logical:test-impl:$(dirname $IMPL_FILE)"
|
|
591
|
+
else
|
|
592
|
+
MATCHED_GROUP="logical:test:$(dirname $file)"
|
|
593
|
+
fi
|
|
594
|
+
;;
|
|
595
|
+
|
|
596
|
+
src/*)
|
|
597
|
+
# Find matching test
|
|
598
|
+
TEST_FILE=$(echo "$file" | sed 's|src/|tests/|; s|\.ts$|.test.ts|')
|
|
599
|
+
if echo "${READY_FILES[@]}" | grep -q "$TEST_FILE"; then
|
|
600
|
+
MATCHED_GROUP="logical:test-impl:$(dirname $file)"
|
|
601
|
+
else
|
|
602
|
+
MATCHED_GROUP="logical:feature:$(dirname $file | sed 's|src/||')"
|
|
603
|
+
fi
|
|
604
|
+
;;
|
|
605
|
+
|
|
606
|
+
package.json|bun.lock|Taskfile.yml|tsconfig.json|*.config.js|*.config.ts)
|
|
607
|
+
MATCHED_GROUP="logical:build"
|
|
608
|
+
;;
|
|
609
|
+
|
|
610
|
+
.github/workflows/*)
|
|
611
|
+
MATCHED_GROUP="logical:ci"
|
|
612
|
+
;;
|
|
613
|
+
|
|
614
|
+
docs/*.md)
|
|
615
|
+
# Try to link to related feature
|
|
616
|
+
DOC_NAME=$(basename "$file" .md)
|
|
617
|
+
FEATURE_FILE="src/${DOC_NAME}.ts"
|
|
618
|
+
if echo "${READY_FILES[@]}" | grep -q "$FEATURE_FILE"; then
|
|
619
|
+
MATCHED_GROUP="logical:feature-docs:${DOC_NAME}"
|
|
620
|
+
else
|
|
621
|
+
MATCHED_GROUP="logical:docs"
|
|
622
|
+
fi
|
|
623
|
+
;;
|
|
624
|
+
|
|
625
|
+
*)
|
|
626
|
+
MATCHED_GROUP="logical:misc"
|
|
627
|
+
;;
|
|
628
|
+
esac
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
FILE_TO_GROUP["$file"]="$MATCHED_GROUP"
|
|
632
|
+
done
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### 3.3 Create Commit Groups
|
|
636
|
+
|
|
637
|
+
```bash
|
|
638
|
+
# Group files by their assigned group
|
|
639
|
+
declare -A GROUP_FILES
|
|
640
|
+
|
|
641
|
+
for file in "${READY_FILES[@]}"; do
|
|
642
|
+
GROUP="${FILE_TO_GROUP[$file]}"
|
|
643
|
+
|
|
644
|
+
if [ -z "${GROUP_FILES[$GROUP]}" ]; then
|
|
645
|
+
GROUP_FILES[$GROUP]="$file"
|
|
646
|
+
else
|
|
647
|
+
GROUP_FILES[$GROUP]="${GROUP_FILES[$GROUP]} $file"
|
|
648
|
+
fi
|
|
649
|
+
done
|
|
650
|
+
|
|
651
|
+
# Sort groups by priority:
|
|
652
|
+
# 1. Task-based groups first
|
|
653
|
+
# 2. Feature+test+docs groups
|
|
654
|
+
# 3. Build/CI groups last
|
|
655
|
+
SORTED_GROUPS=($(for group in "${!GROUP_FILES[@]}"; do echo "$group"; done | \
|
|
656
|
+
sort -t: -k1,1r -k2,2))
|
|
657
|
+
|
|
658
|
+
echo "✅ Grouped ${#READY_FILES[@]} files into ${#SORTED_GROUPS[@]} commits"
|
|
659
|
+
|
|
660
|
+
# Validate grouping
|
|
661
|
+
ASSIGNED_COUNT=0
|
|
662
|
+
for group in "${SORTED_GROUPS[@]}"; do
|
|
663
|
+
FILES="${GROUP_FILES[$group]}"
|
|
664
|
+
FILE_COUNT=$(echo "$FILES" | wc -w | tr -d ' ')
|
|
665
|
+
ASSIGNED_COUNT=$((ASSIGNED_COUNT + FILE_COUNT))
|
|
666
|
+
done
|
|
667
|
+
|
|
668
|
+
if [ $ASSIGNED_COUNT -ne ${#READY_FILES[@]} ]; then
|
|
669
|
+
echo "⚠️ Warning: Grouping mismatch (${ASSIGNED_COUNT} assigned vs ${#READY_FILES[@]} ready)"
|
|
670
|
+
fi
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## Phase 4: Generate Commit Messages
|
|
674
|
+
|
|
675
|
+
**CRITICAL**: NEVER include Task Master task IDs in commit messages. Task IDs are internal to the operator.
|
|
676
|
+
|
|
677
|
+
### 4.1 Generate Message Per Group
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
declare -A COMMIT_MESSAGES
|
|
681
|
+
|
|
682
|
+
for group in "${SORTED_GROUPS[@]}"; do
|
|
683
|
+
GROUP_TYPE=$(echo "$group" | cut -d: -f1)
|
|
684
|
+
GROUP_DETAIL=$(echo "$group" | cut -d: -f2-)
|
|
685
|
+
FILES="${GROUP_FILES[$group]}"
|
|
686
|
+
|
|
687
|
+
# Analyze files to determine commit type and scope
|
|
688
|
+
FILE_ARRAY=($FILES)
|
|
689
|
+
|
|
690
|
+
# Get diff stats
|
|
691
|
+
DIFF_STATS=$(git diff --cached --stat -- "${FILE_ARRAY[@]}")
|
|
692
|
+
DIFF_CONTENT=$(git diff --cached --unified=3 -- "${FILE_ARRAY[@]}" | grep -E '^\+' | head -30)
|
|
693
|
+
|
|
694
|
+
# Determine commit type based on changes
|
|
695
|
+
COMMIT_TYPE="chore"
|
|
696
|
+
|
|
697
|
+
# Check for new functionality
|
|
698
|
+
if echo "$DIFF_CONTENT" | grep -qE '^\+.*function |^\+.*class |^\+.*export '; then
|
|
699
|
+
if echo "$DIFF_CONTENT" | grep -qE 'test.*it\(|describe\('; then
|
|
700
|
+
COMMIT_TYPE="test"
|
|
701
|
+
else
|
|
702
|
+
COMMIT_TYPE="feat"
|
|
703
|
+
fi
|
|
704
|
+
# Check for bug fixes
|
|
705
|
+
elif echo "$DIFF_CONTENT" | grep -qiE '^\+.*(fix|resolve|correct|patch)'; then
|
|
706
|
+
COMMIT_TYPE="fix"
|
|
707
|
+
# Check for documentation
|
|
708
|
+
elif echo "${FILE_ARRAY[*]}" | grep -qE '\.md$'; then
|
|
709
|
+
COMMIT_TYPE="docs"
|
|
710
|
+
# Check for tests
|
|
711
|
+
elif echo "${FILE_ARRAY[*]}" | grep -qE 'test\.ts$'; then
|
|
712
|
+
COMMIT_TYPE="test"
|
|
713
|
+
# Check for build files
|
|
714
|
+
elif echo "${FILE_ARRAY[*]}" | grep -qE 'package\.json|Taskfile\.yml|\.config\.(js|ts)'; then
|
|
715
|
+
COMMIT_TYPE="build"
|
|
716
|
+
# Check for CI files
|
|
717
|
+
elif echo "${FILE_ARRAY[*]}" | grep -qE '\.github/workflows'; then
|
|
718
|
+
COMMIT_TYPE="ci"
|
|
719
|
+
# Check for refactoring
|
|
720
|
+
elif echo "$DIFF_CONTENT" | grep -qE '^\+' && echo "$DIFF_CONTENT" | grep -qE '^\-'; then
|
|
721
|
+
# Roughly equal additions and deletions suggests refactoring
|
|
722
|
+
PLUS_COUNT=$(echo "$DIFF_CONTENT" | grep -cE '^\+')
|
|
723
|
+
MINUS_COUNT=$(git diff --cached --unified=3 -- "${FILE_ARRAY[@]}" | grep -cE '^\-')
|
|
724
|
+
if [ $((PLUS_COUNT - MINUS_COUNT)) -lt 10 ]; then
|
|
725
|
+
COMMIT_TYPE="refactor"
|
|
726
|
+
fi
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
# Determine scope from file paths
|
|
730
|
+
SCOPE=""
|
|
731
|
+
if [ "$GROUP_TYPE" = "task" ]; then
|
|
732
|
+
# Extract scope from task context (WITHOUT exposing task ID)
|
|
733
|
+
TASK_CONTEXT="${TASK_CONTEXTS[$GROUP_DETAIL]}"
|
|
734
|
+
if [ -n "$TASK_CONTEXT" ]; then
|
|
735
|
+
SCOPE=$(echo "$TASK_CONTEXT" | cut -d'|' -f3)
|
|
736
|
+
fi
|
|
737
|
+
elif [ "$GROUP_TYPE" = "logical" ]; then
|
|
738
|
+
# Extract scope from group detail
|
|
739
|
+
case "$GROUP_DETAIL" in
|
|
740
|
+
test-impl:*)
|
|
741
|
+
SCOPE=$(echo "$GROUP_DETAIL" | sed 's|test-impl:||; s|src/||; s|/|:|g')
|
|
742
|
+
;;
|
|
743
|
+
feature:*)
|
|
744
|
+
SCOPE=$(echo "$GROUP_DETAIL" | sed 's|feature:||')
|
|
745
|
+
;;
|
|
746
|
+
feature-docs:*)
|
|
747
|
+
SCOPE=$(echo "$GROUP_DETAIL" | sed 's|feature-docs:||')
|
|
748
|
+
;;
|
|
749
|
+
build|ci|docs|test|misc)
|
|
750
|
+
SCOPE="$GROUP_DETAIL"
|
|
751
|
+
;;
|
|
752
|
+
*)
|
|
753
|
+
# Infer from file paths
|
|
754
|
+
COMMON_PATH=$(echo "${FILE_ARRAY[@]}" | tr ' ' '\n' | \
|
|
755
|
+
sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -1 | \
|
|
756
|
+
awk '{print $2}' | sed 's|src/||; s|tests/||')
|
|
757
|
+
SCOPE="${COMMON_PATH/\//:}"
|
|
758
|
+
;;
|
|
759
|
+
esac
|
|
760
|
+
fi
|
|
761
|
+
|
|
762
|
+
# Generate description
|
|
763
|
+
DESCRIPTION=""
|
|
764
|
+
if [ "$GROUP_TYPE" = "task" ]; then
|
|
765
|
+
# Use task title as basis for description (WITHOUT task ID)
|
|
766
|
+
TASK_CONTEXT="${TASK_CONTEXTS[$GROUP_DETAIL]}"
|
|
767
|
+
if [ -n "$TASK_CONTEXT" ]; then
|
|
768
|
+
TASK_TITLE=$(echo "$TASK_CONTEXT" | cut -d'|' -f1)
|
|
769
|
+
# Convert title to commit description format
|
|
770
|
+
DESCRIPTION=$(echo "$TASK_TITLE" | tr '[:upper:]' '[:lower:]' | \
|
|
771
|
+
sed 's/^implement /add /; s/^create /add /; s/^fix /resolve /')
|
|
772
|
+
fi
|
|
773
|
+
else
|
|
774
|
+
# Generate description from file changes
|
|
775
|
+
if [ ${#FILE_ARRAY[@]} -eq 1 ]; then
|
|
776
|
+
# Single file - describe the change
|
|
777
|
+
FILE_NAME=$(basename "${FILE_ARRAY[0]}")
|
|
778
|
+
DESCRIPTION="update $FILE_NAME"
|
|
779
|
+
else
|
|
780
|
+
# Multiple files - describe the scope
|
|
781
|
+
case "$COMMIT_TYPE" in
|
|
782
|
+
feat)
|
|
783
|
+
DESCRIPTION="add ${SCOPE} functionality"
|
|
784
|
+
;;
|
|
785
|
+
fix)
|
|
786
|
+
DESCRIPTION="resolve ${SCOPE} issues"
|
|
787
|
+
;;
|
|
788
|
+
docs)
|
|
789
|
+
DESCRIPTION="update ${SCOPE} documentation"
|
|
790
|
+
;;
|
|
791
|
+
test)
|
|
792
|
+
DESCRIPTION="add ${SCOPE} tests"
|
|
793
|
+
;;
|
|
794
|
+
build)
|
|
795
|
+
DESCRIPTION="update build configuration"
|
|
796
|
+
;;
|
|
797
|
+
ci)
|
|
798
|
+
DESCRIPTION="update CI workflow"
|
|
799
|
+
;;
|
|
800
|
+
refactor)
|
|
801
|
+
DESCRIPTION="refactor ${SCOPE} implementation"
|
|
802
|
+
;;
|
|
803
|
+
*)
|
|
804
|
+
DESCRIPTION="update ${SCOPE}"
|
|
805
|
+
;;
|
|
806
|
+
esac
|
|
807
|
+
fi
|
|
808
|
+
fi
|
|
809
|
+
|
|
810
|
+
# Generate body with bullet points
|
|
811
|
+
BODY=""
|
|
812
|
+
|
|
813
|
+
# Analyze changes per file for bullet points
|
|
814
|
+
for file in "${FILE_ARRAY[@]}"; do
|
|
815
|
+
FILE_CHANGES=$(git diff --cached --unified=0 "$file" | \
|
|
816
|
+
grep -E '^\+[^+]' | head -5 | sed 's/^\+//')
|
|
817
|
+
|
|
818
|
+
if [ -n "$FILE_CHANGES" ]; then
|
|
819
|
+
FILE_NAME=$(basename "$file")
|
|
820
|
+
# Extract key changes
|
|
821
|
+
if echo "$FILE_CHANGES" | grep -qE 'function |class |export '; then
|
|
822
|
+
FUNCTIONS=$(echo "$FILE_CHANGES" | grep -oE 'function [a-zA-Z]+|class [a-zA-Z]+|export [a-zA-Z]+' | \
|
|
823
|
+
head -3 | sed 's/function /Add function /; s/class /Add class /; s/export /Export /')
|
|
824
|
+
BODY="$BODY\n- $FILE_NAME: $FUNCTIONS"
|
|
825
|
+
elif echo "$FILE_CHANGES" | grep -qE 'test\(|it\(|describe\('; then
|
|
826
|
+
BODY="$BODY\n- Add tests in $FILE_NAME"
|
|
827
|
+
else
|
|
828
|
+
BODY="$BODY\n- Update $FILE_NAME"
|
|
829
|
+
fi
|
|
830
|
+
fi
|
|
831
|
+
done
|
|
832
|
+
|
|
833
|
+
# Compose full message (NO TASK IDS)
|
|
834
|
+
if [ -n "$SCOPE" ]; then
|
|
835
|
+
MESSAGE="${COMMIT_TYPE}(${SCOPE}): ${DESCRIPTION}${BODY}"
|
|
836
|
+
else
|
|
837
|
+
MESSAGE="${COMMIT_TYPE}: ${DESCRIPTION}${BODY}"
|
|
838
|
+
fi
|
|
839
|
+
|
|
840
|
+
COMMIT_MESSAGES["$group"]="$MESSAGE"
|
|
841
|
+
done
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### 4.2 Validate Message Format
|
|
845
|
+
|
|
846
|
+
```bash
|
|
847
|
+
# Validate each generated message
|
|
848
|
+
for group in "${SORTED_GROUPS[@]}"; do
|
|
849
|
+
MESSAGE="${COMMIT_MESSAGES[$group]}"
|
|
850
|
+
FIRST_LINE=$(echo -e "$MESSAGE" | head -1)
|
|
851
|
+
|
|
852
|
+
# Ensure conventional commit format
|
|
853
|
+
if ! echo "$FIRST_LINE" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?: .+'; then
|
|
854
|
+
echo "⚠️ Generated message doesn't follow conventional format"
|
|
855
|
+
echo "Group: $group"
|
|
856
|
+
echo "Message: $FIRST_LINE"
|
|
857
|
+
echo "Adjusting format..."
|
|
858
|
+
fi
|
|
859
|
+
|
|
860
|
+
# Check first line length (≤72 characters)
|
|
861
|
+
if [ ${#FIRST_LINE} -gt 72 ]; then
|
|
862
|
+
echo "⚠️ Message too long (${#FIRST_LINE} chars), truncating to 72"
|
|
863
|
+
FIRST_LINE="${FIRST_LINE:0:69}..."
|
|
864
|
+
# Update message
|
|
865
|
+
BODY=$(echo -e "$MESSAGE" | tail -n +2)
|
|
866
|
+
COMMIT_MESSAGES["$group"]="${FIRST_LINE}${BODY}"
|
|
867
|
+
fi
|
|
868
|
+
|
|
869
|
+
# CRITICAL: Ensure no task IDs in message
|
|
870
|
+
if echo "$MESSAGE" | grep -qE 'Task: #|task #[0-9]'; then
|
|
871
|
+
echo "❌ CRITICAL: Task ID found in commit message!"
|
|
872
|
+
echo "Group: $group"
|
|
873
|
+
echo "Task IDs are internal to the operator and must not be in commits"
|
|
874
|
+
exit 1
|
|
875
|
+
fi
|
|
876
|
+
done
|
|
877
|
+
|
|
878
|
+
echo "✅ All ${#SORTED_GROUPS[@]} commit messages validated"
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
## Phase 5: Pre-Commit QA Checks
|
|
882
|
+
|
|
883
|
+
### 5.1 Run Validation Tasks
|
|
884
|
+
|
|
885
|
+
```bash
|
|
886
|
+
# Discover available tasks
|
|
887
|
+
AVAILABLE_TASKS=()
|
|
888
|
+
if [ -f Taskfile.yml ]; then
|
|
889
|
+
AVAILABLE_TASKS=($(task --list 2>/dev/null | grep -E 'lint|typecheck|test' | awk '{print $2}'))
|
|
890
|
+
fi
|
|
891
|
+
|
|
892
|
+
# Run linting
|
|
893
|
+
LINT_STATUS="⊘ Skipped"
|
|
894
|
+
LINT_FAILED=false
|
|
895
|
+
if echo "${AVAILABLE_TASKS[@]}" | grep -q '^lint$'; then
|
|
896
|
+
echo "🔍 Running lint checks..."
|
|
897
|
+
if task lint; then
|
|
898
|
+
LINT_STATUS="✅ Passed"
|
|
899
|
+
else
|
|
900
|
+
LINT_STATUS="❌ Failed"
|
|
901
|
+
LINT_FAILED=true
|
|
902
|
+
fi
|
|
903
|
+
fi
|
|
904
|
+
|
|
905
|
+
# Run type checking
|
|
906
|
+
TYPECHECK_STATUS="⊘ Skipped"
|
|
907
|
+
TYPECHECK_FAILED=false
|
|
908
|
+
if echo "${AVAILABLE_TASKS[@]}" | grep -q '^typecheck$'; then
|
|
909
|
+
echo "🔍 Running type checks..."
|
|
910
|
+
if task typecheck; then
|
|
911
|
+
TYPECHECK_STATUS="✅ Passed"
|
|
912
|
+
else
|
|
913
|
+
TYPECHECK_STATUS="❌ Failed"
|
|
914
|
+
TYPECHECK_FAILED=true
|
|
915
|
+
fi
|
|
916
|
+
elif command -v tsc &>/dev/null; then
|
|
917
|
+
echo "🔍 Running TypeScript type checks..."
|
|
918
|
+
if tsc --noEmit; then
|
|
919
|
+
TYPECHECK_STATUS="✅ Passed"
|
|
920
|
+
else
|
|
921
|
+
TYPECHECK_STATUS="❌ Failed"
|
|
922
|
+
TYPECHECK_FAILED=true
|
|
923
|
+
fi
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
# Run tests (skip in dry-run)
|
|
927
|
+
TEST_STATUS="⊘ Skipped"
|
|
928
|
+
TEST_FAILED=false
|
|
929
|
+
if [ "$DRY_RUN" != "true" ] && echo "${AVAILABLE_TASKS[@]}" | grep -q '^test$'; then
|
|
930
|
+
echo "🧪 Running tests..."
|
|
931
|
+
if task test; then
|
|
932
|
+
TEST_STATUS="✅ Passed"
|
|
933
|
+
else
|
|
934
|
+
TEST_STATUS="❌ Failed"
|
|
935
|
+
TEST_FAILED=true
|
|
936
|
+
fi
|
|
937
|
+
fi
|
|
938
|
+
|
|
939
|
+
# Handle QA failures
|
|
940
|
+
if [ "$LINT_FAILED" = "true" ] || [ "$TYPECHECK_FAILED" = "true" ] || [ "$TEST_FAILED" = "true" ]; then
|
|
941
|
+
echo ""
|
|
942
|
+
echo "❌ Pre-commit QA checks failed"
|
|
943
|
+
echo ""
|
|
944
|
+
[ "$LINT_FAILED" = "true" ] && echo " - Linting: Fix with 'task lint:fix'"
|
|
945
|
+
[ "$TYPECHECK_FAILED" = "true" ] && echo " - Type errors: Review tsc output"
|
|
946
|
+
[ "$TEST_FAILED" = "true" ] && echo " - Tests: Fix failing tests"
|
|
947
|
+
echo ""
|
|
948
|
+
echo "Fix issues and retry commit"
|
|
949
|
+
exit 1
|
|
950
|
+
fi
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
## Phase 6: Execute Commits
|
|
954
|
+
|
|
955
|
+
### 6.1 Dry-Run Mode
|
|
956
|
+
|
|
957
|
+
```bash
|
|
958
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
959
|
+
echo ""
|
|
960
|
+
echo "🔍 DRY RUN - No commits will be made"
|
|
961
|
+
echo ""
|
|
962
|
+
echo "📊 Would create ${#SORTED_GROUPS[@]} commits for ${#READY_FILES[@]} files:"
|
|
963
|
+
echo ""
|
|
964
|
+
|
|
965
|
+
COMMIT_NUM=1
|
|
966
|
+
for group in "${SORTED_GROUPS[@]}"; do
|
|
967
|
+
FILES="${GROUP_FILES[$group]}"
|
|
968
|
+
FILE_COUNT=$(echo "$FILES" | wc -w | tr -d ' ')
|
|
969
|
+
MESSAGE="${COMMIT_MESSAGES[$group]}"
|
|
970
|
+
FIRST_LINE=$(echo -e "$MESSAGE" | head -1)
|
|
971
|
+
|
|
972
|
+
echo "Commit $COMMIT_NUM of ${#SORTED_GROUPS[@]}: $FIRST_LINE"
|
|
973
|
+
echo " Files ($FILE_COUNT):"
|
|
974
|
+
for file in $FILES; do
|
|
975
|
+
STATUS=$(git status --porcelain "$file" | awk '{print $1}')
|
|
976
|
+
echo " - $file ($STATUS)"
|
|
977
|
+
done
|
|
978
|
+
echo ""
|
|
979
|
+
((COMMIT_NUM++))
|
|
980
|
+
done
|
|
981
|
+
|
|
982
|
+
echo "💡 Run without --dry-run to commit"
|
|
983
|
+
exit 0
|
|
984
|
+
fi
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### 6.2 Create Commits Sequentially
|
|
988
|
+
|
|
989
|
+
```bash
|
|
990
|
+
COMMIT_NUM=1
|
|
991
|
+
CREATED_COMMITS=()
|
|
992
|
+
FAILED_COMMITS=()
|
|
993
|
+
|
|
994
|
+
for group in "${SORTED_GROUPS[@]}"; do
|
|
995
|
+
FILES="${GROUP_FILES[$group]}"
|
|
996
|
+
MESSAGE="${COMMIT_MESSAGES[$group]}"
|
|
997
|
+
FIRST_LINE=$(echo -e "$MESSAGE" | head -1)
|
|
998
|
+
|
|
999
|
+
echo ""
|
|
1000
|
+
echo "📝 Creating commit $COMMIT_NUM of ${#SORTED_GROUPS[@]}: $FIRST_LINE"
|
|
1001
|
+
|
|
1002
|
+
# Reset staging area
|
|
1003
|
+
git reset >/dev/null 2>&1 || true
|
|
1004
|
+
|
|
1005
|
+
# Stage only files for this group
|
|
1006
|
+
for file in $FILES; do
|
|
1007
|
+
git add "$file" || {
|
|
1008
|
+
echo "❌ Failed to stage $file"
|
|
1009
|
+
FAILED_COMMITS+=("$group:Failed to stage files")
|
|
1010
|
+
break 2
|
|
1011
|
+
}
|
|
1012
|
+
done
|
|
1013
|
+
|
|
1014
|
+
# Create commit
|
|
1015
|
+
NO_VERIFY_FLAG=""
|
|
1016
|
+
[ "$NO_VERIFY" = "true" ] && NO_VERIFY_FLAG="--no-verify"
|
|
1017
|
+
|
|
1018
|
+
if git commit $NO_VERIFY_FLAG -m "$(echo -e "$MESSAGE")"; then
|
|
1019
|
+
COMMIT_SHA=$(git rev-parse HEAD)
|
|
1020
|
+
CREATED_COMMITS+=("$group:$COMMIT_SHA")
|
|
1021
|
+
echo "✅ Committed: $COMMIT_SHA"
|
|
1022
|
+
else
|
|
1023
|
+
FAILED_COMMITS+=("$group:Commit failed (likely hooks)")
|
|
1024
|
+
echo "❌ Commit failed for group: $group"
|
|
1025
|
+
echo " Stopping at commit $COMMIT_NUM of ${#SORTED_GROUPS[@]}"
|
|
1026
|
+
break
|
|
1027
|
+
fi
|
|
1028
|
+
|
|
1029
|
+
((COMMIT_NUM++))
|
|
1030
|
+
done
|
|
1031
|
+
|
|
1032
|
+
# Handle commit failures
|
|
1033
|
+
if [ ${#FAILED_COMMITS[@]} -gt 0 ]; then
|
|
1034
|
+
echo ""
|
|
1035
|
+
echo "❌ Commit sequence failed"
|
|
1036
|
+
echo ""
|
|
1037
|
+
echo "Created: ${#CREATED_COMMITS[@]} commits"
|
|
1038
|
+
echo "Failed: ${#FAILED_COMMITS[@]} commits"
|
|
1039
|
+
echo ""
|
|
1040
|
+
echo "Options:"
|
|
1041
|
+
echo " 1. Fix issues and run /commit again"
|
|
1042
|
+
echo " 2. Use 'git reset HEAD~${#CREATED_COMMITS[@]}' to undo all commits"
|
|
1043
|
+
echo ""
|
|
1044
|
+
exit 1
|
|
1045
|
+
fi
|
|
1046
|
+
|
|
1047
|
+
echo ""
|
|
1048
|
+
echo "✅ All commits successful (${#CREATED_COMMITS[@]} commits created)"
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
## Phase 7: Generate Report
|
|
1052
|
+
|
|
1053
|
+
```bash
|
|
1054
|
+
REPORT_FILE=".commit.report.local.md"
|
|
1055
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1056
|
+
|
|
1057
|
+
cat > "$REPORT_FILE" <<EOF
|
|
1058
|
+
# Commit Report
|
|
1059
|
+
|
|
1060
|
+
**Generated:** $TIMESTAMP
|
|
1061
|
+
**Branch:** $(git branch --show-current)
|
|
1062
|
+
**Operator:** $(git config user.name) <$(git config user.email)>
|
|
1063
|
+
|
|
1064
|
+
## Summary
|
|
1065
|
+
|
|
1066
|
+
**Files Analyzed:** $TOTAL_FILES
|
|
1067
|
+
**Files Committed:** ${#READY_FILES[@]}
|
|
1068
|
+
**Files Skipped:** ${#SKIPPED_FILES[@]}
|
|
1069
|
+
**Commits Created:** ${#CREATED_COMMITS[@]}
|
|
1070
|
+
|
|
1071
|
+
## Commits Created
|
|
1072
|
+
|
|
1073
|
+
$(for item in "${CREATED_COMMITS[@]}"; do
|
|
1074
|
+
GROUP=$(echo "$item" | cut -d: -f1)
|
|
1075
|
+
SHA=$(echo "$item" | cut -d: -f2)
|
|
1076
|
+
MESSAGE="${COMMIT_MESSAGES[$GROUP]}"
|
|
1077
|
+
FIRST_LINE=$(echo -e "$MESSAGE" | head -1)
|
|
1078
|
+
FILES="${GROUP_FILES[$GROUP]}"
|
|
1079
|
+
FILE_COUNT=$(echo "$FILES" | wc -w | tr -d ' ')
|
|
1080
|
+
echo "### Commit: \`$SHA\`"
|
|
1081
|
+
echo ""
|
|
1082
|
+
echo "\`\`\`"
|
|
1083
|
+
echo -e "$MESSAGE"
|
|
1084
|
+
echo "\`\`\`"
|
|
1085
|
+
echo ""
|
|
1086
|
+
echo "**Files ($FILE_COUNT):**"
|
|
1087
|
+
echo ""
|
|
1088
|
+
for file in $FILES; do
|
|
1089
|
+
STAT=\$(git diff HEAD~1 --numstat -- "$file" 2>/dev/null | awk '{print "+"$1", -"$2}')
|
|
1090
|
+
echo "- \`$file\` ($STAT)"
|
|
1091
|
+
done
|
|
1092
|
+
echo ""
|
|
1093
|
+
done)
|
|
1094
|
+
|
|
1095
|
+
## Skipped Files
|
|
1096
|
+
|
|
1097
|
+
$(if [ ${#SKIPPED_FILES[@]} -eq 0 ]; then
|
|
1098
|
+
echo "No files skipped ✅"
|
|
1099
|
+
else
|
|
1100
|
+
echo "**Total Skipped:** ${#SKIPPED_FILES[@]}"
|
|
1101
|
+
echo ""
|
|
1102
|
+
for item in "${SKIPPED_FILES[@]}"; do
|
|
1103
|
+
file=$(echo "$item" | cut -d: -f1)
|
|
1104
|
+
reason=$(echo "$item" | cut -d: -f2-)
|
|
1105
|
+
echo "- \`$file\`: $reason"
|
|
1106
|
+
done
|
|
1107
|
+
fi)
|
|
1108
|
+
|
|
1109
|
+
## Pre-Commit Checks
|
|
1110
|
+
|
|
1111
|
+
- **Linting:** $LINT_STATUS
|
|
1112
|
+
- **Type Checking:** $TYPECHECK_STATUS
|
|
1113
|
+
- **Tests:** $TEST_STATUS
|
|
1114
|
+
|
|
1115
|
+
## Next Steps
|
|
1116
|
+
|
|
1117
|
+
$(AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "0")
|
|
1118
|
+
if [ "$AHEAD" -gt 0 ]; then
|
|
1119
|
+
echo "**Branch Status:** Branch is ahead by $AHEAD commit(s)"
|
|
1120
|
+
echo "**Action Required:** Use \`git push\` to publish changes"
|
|
1121
|
+
else
|
|
1122
|
+
echo "Branch is up to date with remote"
|
|
1123
|
+
fi)
|
|
1124
|
+
|
|
1125
|
+
---
|
|
1126
|
+
|
|
1127
|
+
**Report Location:** \`.commit.report.local.md\`
|
|
1128
|
+
EOF
|
|
1129
|
+
|
|
1130
|
+
# Add report to gitignore
|
|
1131
|
+
if ! grep -q "^\.commit\.report\.local\.md$" .gitignore 2>/dev/null; then
|
|
1132
|
+
echo ".commit.report.local.md" >> .gitignore
|
|
1133
|
+
fi
|
|
1134
|
+
|
|
1135
|
+
echo ""
|
|
1136
|
+
echo "📄 Report saved: .commit.report.local.md"
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
## Phase 8: Present Results
|
|
1140
|
+
|
|
1141
|
+
```bash
|
|
1142
|
+
echo ""
|
|
1143
|
+
echo "✅ Commit process complete"
|
|
1144
|
+
echo ""
|
|
1145
|
+
echo "📊 Summary:"
|
|
1146
|
+
echo " - Files committed: ${#READY_FILES[@]}"
|
|
1147
|
+
echo " - Commits created: ${#CREATED_COMMITS[@]}"
|
|
1148
|
+
echo " - Branch: $(git branch --show-current)"
|
|
1149
|
+
echo ""
|
|
1150
|
+
echo "📝 Commits:"
|
|
1151
|
+
for item in "${CREATED_COMMITS[@]}"; do
|
|
1152
|
+
GROUP=$(echo "$item" | cut -d: -f1)
|
|
1153
|
+
SHA=$(echo "$item" | cut -d: -f2)
|
|
1154
|
+
MESSAGE="${COMMIT_MESSAGES[$GROUP]}"
|
|
1155
|
+
FIRST_LINE=$(echo -e "$MESSAGE" | head -1)
|
|
1156
|
+
echo " - ${SHA:0:7}: $FIRST_LINE"
|
|
1157
|
+
done
|
|
1158
|
+
echo ""
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
## Important Notes
|
|
1162
|
+
|
|
1163
|
+
### Task Master Integration
|
|
1164
|
+
|
|
1165
|
+
**CRITICAL**: Task IDs are internal to the operator and MUST NOT appear in commit messages.
|
|
1166
|
+
|
|
1167
|
+
**Correct usage:**
|
|
1168
|
+
|
|
1169
|
+
- Use Task Master to understand what work is being done
|
|
1170
|
+
- Match files to task contexts based on title, description, scope
|
|
1171
|
+
- Generate commit messages from task context WITHOUT task IDs
|
|
1172
|
+
- Example: "feat(cli): add selective tool installation" (no task ID)
|
|
1173
|
+
|
|
1174
|
+
**Incorrect usage:**
|
|
1175
|
+
|
|
1176
|
+
- ❌ "feat(cli): implement task 1.2 - selective install"
|
|
1177
|
+
- ❌ "fix(paths): resolve issue in task 5"
|
|
1178
|
+
- ❌ "Task: #1.2" in commit footer
|
|
1179
|
+
|
|
1180
|
+
### Grouping Philosophy
|
|
1181
|
+
|
|
1182
|
+
**Files that belong together:**
|
|
1183
|
+
|
|
1184
|
+
- Implementation + corresponding tests
|
|
1185
|
+
- Feature + documentation
|
|
1186
|
+
- Related bug fixes
|
|
1187
|
+
- Build configuration changes
|
|
1188
|
+
|
|
1189
|
+
**Files that should be separate commits:**
|
|
1190
|
+
|
|
1191
|
+
- Unrelated features
|
|
1192
|
+
- Different bug fixes
|
|
1193
|
+
- Independent refactorings
|
|
1194
|
+
- Feature + unrelated dependency updates
|
|
1195
|
+
|
|
1196
|
+
### Hook Compliance
|
|
1197
|
+
|
|
1198
|
+
**Always respect git hooks:**
|
|
1199
|
+
|
|
1200
|
+
- Husky pre-commit hooks validate code quality
|
|
1201
|
+
- Commit-msg hooks enforce conventional commits
|
|
1202
|
+
- Never use `--no-verify` unless explicitly approved by user
|
|
1203
|
+
- Fix issues rather than bypassing hooks
|
|
1204
|
+
|
|
1205
|
+
### Multi-Commit Benefits
|
|
1206
|
+
|
|
1207
|
+
**Why create multiple commits:**
|
|
1208
|
+
|
|
1209
|
+
- Clear history - each commit has single purpose
|
|
1210
|
+
- Easy review - reviewers understand changes per commit
|
|
1211
|
+
- Better rollback - revert specific changes without affecting others
|
|
1212
|
+
- Semantic grouping - related changes stay together
|
|
1213
|
+
|
|
1214
|
+
**When to use single commit:**
|
|
1215
|
+
|
|
1216
|
+
- User provides `--message` flag (bypasses grouping)
|
|
1217
|
+
- Small changesets (< 5 files, all related)
|
|
1218
|
+
- Trivial changes (formatting, typos, dependency updates)
|