@engineereddev/fractal-planner 0.1.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/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/agents/fp-analyst.md +96 -0
- package/agents/fp-context-builder.md +87 -0
- package/agents/fp-critic.md +140 -0
- package/agents/fp-decomposer.md +261 -0
- package/agents/fp-interviewer.md +263 -0
- package/agents/fp-linear-sync.md +128 -0
- package/agents/fp-researcher.md +82 -0
- package/agents/fp-task-tracker.md +134 -0
- package/dist/cli/classify-intent.js +118 -0
- package/dist/cli/compute-signals.js +495 -0
- package/dist/cli/generate-plan.js +14209 -0
- package/dist/cli/load-config.js +13661 -0
- package/dist/cli/validate-tasks.js +467 -0
- package/dist/index.js +24598 -0
- package/dist/src/cli/classify-intent.d.ts +3 -0
- package/dist/src/cli/compute-signals.d.ts +14 -0
- package/dist/src/cli/generate-plan.d.ts +3 -0
- package/dist/src/cli/load-config.d.ts +3 -0
- package/dist/src/cli/validate-tasks.d.ts +3 -0
- package/dist/src/config.d.ts +182 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/phases/clearance.d.ts +12 -0
- package/dist/src/phases/decomposition.d.ts +41 -0
- package/dist/src/phases/interview.d.ts +17 -0
- package/dist/src/phases/planning.d.ts +21 -0
- package/dist/src/phases/research.d.ts +9 -0
- package/dist/src/types/index.d.ts +116 -0
- package/dist/src/utils/draft.d.ts +21 -0
- package/dist/src/utils/question-strategies.d.ts +24 -0
- package/dist/src/utils/task-parser.d.ts +3 -0
- package/hooks/hooks.json +27 -0
- package/hooks/nudge-teammate.sh +216 -0
- package/hooks/run-comment-checker.sh +91 -0
- package/package.json +65 -0
- package/skills/commit/SKILL.md +157 -0
- package/skills/fp/SKILL.md +857 -0
- package/skills/fp/scripts/resolve-env.sh +66 -0
- package/skills/handoff/SKILL.md +195 -0
- package/skills/implement/SKILL.md +783 -0
- package/skills/implement/reference.md +935 -0
- package/skills/retry/SKILL.md +333 -0
- package/skills/status/SKILL.md +182 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# TeammateIdle hook: detects stalled teammates and re-injects continuation prompts.
|
|
5
|
+
# Handles two teammate types:
|
|
6
|
+
# - builder-* on fp-impl-* teams (task-based stall detection)
|
|
7
|
+
# - interviewer on fp-interview-* teams (consecutive idle stall detection)
|
|
8
|
+
#
|
|
9
|
+
# Exit 0 = no action (hook passes through).
|
|
10
|
+
# Exit 2 + stderr = re-inject continuation prompt into the teammate.
|
|
11
|
+
|
|
12
|
+
# --- Read stdin (TeammateIdle hook input) ---
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
|
|
15
|
+
# --- Extract fields via python3 ---
|
|
16
|
+
TEAMMATE_NAME=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('teammate_name',''))" 2>/dev/null || echo "")
|
|
17
|
+
TEAM_NAME=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('team_name',''))" 2>/dev/null || echo "")
|
|
18
|
+
CWD=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "")
|
|
19
|
+
|
|
20
|
+
# --- Determine teammate type ---
|
|
21
|
+
IS_BUILDER=false
|
|
22
|
+
IS_INTERVIEWER=false
|
|
23
|
+
|
|
24
|
+
if [[ "$TEAM_NAME" == fp-impl-* ]] && [[ "$TEAMMATE_NAME" == builder-* ]]; then
|
|
25
|
+
IS_BUILDER=true
|
|
26
|
+
elif [[ "$TEAM_NAME" == fp-interview-* ]] && [[ "$TEAMMATE_NAME" == "interviewer" ]]; then
|
|
27
|
+
IS_INTERVIEWER=true
|
|
28
|
+
else
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# --- Read merged config (user + project, matching TypeScript loadConfig merge order) ---
|
|
33
|
+
NUDGE_CFG=$(python3 << PYEOF
|
|
34
|
+
import json, os
|
|
35
|
+
def rj(p):
|
|
36
|
+
try:
|
|
37
|
+
with open(p) as f: return json.load(f)
|
|
38
|
+
except: return {}
|
|
39
|
+
xdg = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
|
40
|
+
cwd = "${CWD:-.}"
|
|
41
|
+
m = {**rj(os.path.join(xdg, 'fractal-planner', 'config.json')),
|
|
42
|
+
**rj(os.path.join(cwd, '.fractal-planner', 'config.json'))}
|
|
43
|
+
n = m.get('nudge', {})
|
|
44
|
+
print(f"{str(n.get('enabled', True)).lower()}|{n.get('maxRetries', 3)}")
|
|
45
|
+
PYEOF
|
|
46
|
+
) || NUDGE_CFG="true|3"
|
|
47
|
+
IFS='|' read -r NUDGE_ENABLED MAX_RETRIES <<< "$NUDGE_CFG"
|
|
48
|
+
|
|
49
|
+
if [[ "$NUDGE_ENABLED" == "false" ]]; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# --- State directory and file ---
|
|
54
|
+
STATE_DIR="$HOME/.claude/teams/${TEAM_NAME}"
|
|
55
|
+
STATE_FILE="${STATE_DIR}/nudge-${TEAMMATE_NAME}.json"
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# BUILDER PATH: task-based stall detection (unchanged logic)
|
|
59
|
+
# =============================================================================
|
|
60
|
+
if [[ "$IS_BUILDER" == true ]]; then
|
|
61
|
+
TASKS_DIR="$HOME/.claude/tasks/${TEAM_NAME}"
|
|
62
|
+
if [[ ! -d "$TASKS_DIR" ]]; then
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
MATCHED_TASK=""
|
|
67
|
+
MATCHED_NATIVE_ID=""
|
|
68
|
+
MATCHED_PLAN_TASK_ID=""
|
|
69
|
+
|
|
70
|
+
for TASK_FILE in "$TASKS_DIR"/*.json; do
|
|
71
|
+
[[ -f "$TASK_FILE" ]] || continue
|
|
72
|
+
|
|
73
|
+
TASK_INFO=$(python3 -c "
|
|
74
|
+
import json,sys
|
|
75
|
+
try:
|
|
76
|
+
t=json.load(open('$TASK_FILE'))
|
|
77
|
+
status=t.get('status','')
|
|
78
|
+
owner=t.get('owner','')
|
|
79
|
+
internal=t.get('metadata',{}).get('_internal',False)
|
|
80
|
+
task_id=t.get('id','')
|
|
81
|
+
subject=t.get('subject','')
|
|
82
|
+
fp_status=t.get('metadata',{}).get('fpStatus','')
|
|
83
|
+
print(f'{status}|{owner}|{internal}|{task_id}|{subject}|{fp_status}')
|
|
84
|
+
except: print('|||||')
|
|
85
|
+
" 2>/dev/null || echo "|||||")
|
|
86
|
+
|
|
87
|
+
IFS='|' read -r T_STATUS T_OWNER T_INTERNAL T_ID T_SUBJECT T_FP_STATUS <<< "$TASK_INFO"
|
|
88
|
+
|
|
89
|
+
if [[ "$T_STATUS" == "in_progress" ]] && [[ "$T_OWNER" == "$TEAMMATE_NAME" ]] && [[ "$T_INTERNAL" != "True" ]]; then
|
|
90
|
+
MATCHED_NATIVE_ID="$T_ID"
|
|
91
|
+
MATCHED_PLAN_TASK_ID=$(echo "$T_SUBJECT" | python3 -c "
|
|
92
|
+
import sys,re
|
|
93
|
+
s=sys.stdin.read().strip()
|
|
94
|
+
m=re.match(r'\[([^\]]+)\]',s)
|
|
95
|
+
print(m.group(1) if m else '')
|
|
96
|
+
" 2>/dev/null || echo "")
|
|
97
|
+
MATCHED_FP_STATUS="$T_FP_STATUS"
|
|
98
|
+
MATCHED_TASK="$TASK_FILE"
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
if [[ -z "$MATCHED_TASK" ]]; then
|
|
104
|
+
rm -f "$STATE_FILE"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Builder is waiting for lead's verification response — not stalled
|
|
109
|
+
if [[ "$MATCHED_FP_STATUS" == "AWAITING_VERIFICATION" ]]; then
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Read or initialize state
|
|
114
|
+
CURRENT_RETRIES=0
|
|
115
|
+
CURRENT_TASK_ID=""
|
|
116
|
+
|
|
117
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
118
|
+
STATE_INFO=$(python3 -c "
|
|
119
|
+
import json,sys
|
|
120
|
+
try:
|
|
121
|
+
s=json.load(open('$STATE_FILE'))
|
|
122
|
+
print(f'{s.get(\"retries\",0)}|{s.get(\"taskId\",\"\")}')
|
|
123
|
+
except: print('0|')
|
|
124
|
+
" 2>/dev/null || echo "0|")
|
|
125
|
+
|
|
126
|
+
IFS='|' read -r CURRENT_RETRIES CURRENT_TASK_ID <<< "$STATE_INFO"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if [[ "$CURRENT_TASK_ID" != "$MATCHED_NATIVE_ID" ]]; then
|
|
130
|
+
CURRENT_RETRIES=0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [[ "$CURRENT_RETRIES" -ge "$MAX_RETRIES" ]]; then
|
|
134
|
+
rm -f "$STATE_FILE"
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
CURRENT_RETRIES=$((CURRENT_RETRIES + 1))
|
|
139
|
+
mkdir -p "$STATE_DIR"
|
|
140
|
+
|
|
141
|
+
python3 -c "
|
|
142
|
+
import json
|
|
143
|
+
state = {
|
|
144
|
+
'retries': $CURRENT_RETRIES,
|
|
145
|
+
'lastRetryAt': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
|
|
146
|
+
'taskId': '$MATCHED_NATIVE_ID'
|
|
147
|
+
}
|
|
148
|
+
with open('$STATE_FILE', 'w') as f:
|
|
149
|
+
json.dump(state, f, indent=2)
|
|
150
|
+
" 2>/dev/null
|
|
151
|
+
|
|
152
|
+
cat >&2 <<PROMPT
|
|
153
|
+
You are ${TEAMMATE_NAME} and you have an in-progress task [${MATCHED_PLAN_TASK_ID}] that you have not completed.
|
|
154
|
+
You appear to have stalled (idle detection, attempt ${CURRENT_RETRIES}/${MAX_RETRIES}).
|
|
155
|
+
|
|
156
|
+
Resume your self-claiming work loop:
|
|
157
|
+
1. You already own task ${MATCHED_PLAN_TASK_ID} (native ID: ${MATCHED_NATIVE_ID}). Call TaskGet(${MATCHED_NATIVE_ID}) to re-read the task spec.
|
|
158
|
+
2. Continue implementing the task according to the spec.
|
|
159
|
+
3. When done, send "IMPLEMENTATION COMPLETE: ${MATCHED_PLAN_TASK_ID}" with FILES_MODIFIED to "team-lead".
|
|
160
|
+
|
|
161
|
+
If you previously sent IMPLEMENTATION COMPLETE and are waiting for verification, send the message again -- the lead may have missed it.
|
|
162
|
+
PROMPT
|
|
163
|
+
|
|
164
|
+
exit 2
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# INTERVIEWER PATH: consecutive idle stall detection
|
|
169
|
+
# =============================================================================
|
|
170
|
+
if [[ "$IS_INTERVIEWER" == true ]]; then
|
|
171
|
+
CURRENT_RETRIES=0
|
|
172
|
+
|
|
173
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
174
|
+
CURRENT_RETRIES=$(python3 -c "
|
|
175
|
+
import json,sys
|
|
176
|
+
try:
|
|
177
|
+
s=json.load(open('$STATE_FILE'))
|
|
178
|
+
print(s.get('retries',0))
|
|
179
|
+
except: print('0')
|
|
180
|
+
" 2>/dev/null || echo "0")
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if [[ "$CURRENT_RETRIES" -ge "$MAX_RETRIES" ]]; then
|
|
184
|
+
rm -f "$STATE_FILE"
|
|
185
|
+
exit 0
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
CURRENT_RETRIES=$((CURRENT_RETRIES + 1))
|
|
189
|
+
mkdir -p "$STATE_DIR"
|
|
190
|
+
|
|
191
|
+
python3 -c "
|
|
192
|
+
import json
|
|
193
|
+
state = {
|
|
194
|
+
'retries': $CURRENT_RETRIES,
|
|
195
|
+
'lastRetryAt': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat()
|
|
196
|
+
}
|
|
197
|
+
with open('$STATE_FILE', 'w') as f:
|
|
198
|
+
json.dump(state, f, indent=2)
|
|
199
|
+
" 2>/dev/null
|
|
200
|
+
|
|
201
|
+
cat >&2 <<PROMPT
|
|
202
|
+
You are the interviewer and you appear to have stalled (idle detection, attempt ${CURRENT_RETRIES}/${MAX_RETRIES}).
|
|
203
|
+
|
|
204
|
+
You MUST complete your mandatory draft update loop. Do one of the following:
|
|
205
|
+
|
|
206
|
+
1. If you have pending questions for the user, format and send them as a QUESTIONS message to the lead.
|
|
207
|
+
2. If you are evaluating clearance, finish the evaluation and send either:
|
|
208
|
+
- "DRAFT UPDATED (Round N)" with clearance status, then follow up with QUESTIONS if gaps remain
|
|
209
|
+
- "CLEARANCE ACHIEVED" if all 6 clearance criteria are met
|
|
210
|
+
3. If you already sent a message and are waiting for a response, send it again -- the lead may have missed it.
|
|
211
|
+
|
|
212
|
+
Do NOT go idle without sending a protocol message (QUESTIONS, DRAFT UPDATED, or CLEARANCE ACHIEVED).
|
|
213
|
+
PROMPT
|
|
214
|
+
|
|
215
|
+
exit 2
|
|
216
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# PostToolUse hook wrapper for comment-checker.
|
|
5
|
+
# Reads tool_input from stdin, runs the comment-checker binary,
|
|
6
|
+
# and outputs a JSON systemMessage if unnecessary comments are detected.
|
|
7
|
+
#
|
|
8
|
+
# Exit 0 always — hooks must never block Claude Code.
|
|
9
|
+
|
|
10
|
+
# --- Read merged config (user + project, matching TypeScript loadConfig merge order) ---
|
|
11
|
+
eval "$(python3 << 'PYEOF'
|
|
12
|
+
import json, os, shlex
|
|
13
|
+
def rj(p):
|
|
14
|
+
try:
|
|
15
|
+
with open(p) as f: return json.load(f)
|
|
16
|
+
except: return {}
|
|
17
|
+
xdg = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
|
18
|
+
m = {**rj(os.path.join(xdg, 'fractal-planner', 'config.json')), **rj('.fractal-planner/config.json')}
|
|
19
|
+
cc = m.get('commentChecker', {})
|
|
20
|
+
print(f"CFG_ENABLED={shlex.quote(str(cc.get('enabled', True)).lower())}")
|
|
21
|
+
print(f"CFG_BINARY_PATH={shlex.quote(cc.get('binaryPath', ''))}")
|
|
22
|
+
print(f"CFG_CUSTOM_PROMPT={shlex.quote(cc.get('customPrompt', ''))}")
|
|
23
|
+
PYEOF
|
|
24
|
+
)" || { CFG_ENABLED="true"; CFG_BINARY_PATH=""; CFG_CUSTOM_PROMPT=""; }
|
|
25
|
+
|
|
26
|
+
# --- Check disabled ---
|
|
27
|
+
if [[ "$CFG_ENABLED" == "false" ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# --- Locate binary (config > node_modules > PATH > cache dirs) ---
|
|
32
|
+
BINARY=""
|
|
33
|
+
|
|
34
|
+
if [[ -n "$CFG_BINARY_PATH" ]] && [[ -x "$CFG_BINARY_PATH" ]]; then
|
|
35
|
+
BINARY="$CFG_BINARY_PATH"
|
|
36
|
+
else
|
|
37
|
+
# Resolve plugin root (directory containing hooks/)
|
|
38
|
+
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
39
|
+
CANDIDATE="$PLUGIN_ROOT/node_modules/.bin/comment-checker"
|
|
40
|
+
if [[ -x "$CANDIDATE" ]]; then
|
|
41
|
+
BINARY="$CANDIDATE"
|
|
42
|
+
elif command -v comment-checker &>/dev/null; then
|
|
43
|
+
BINARY="$(command -v comment-checker)"
|
|
44
|
+
else
|
|
45
|
+
# Check XDG cache directories
|
|
46
|
+
XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
47
|
+
for dir in "$XDG_CACHE/fractal-planner/bin" "$XDG_CACHE/oh-my-opencode/bin"; do
|
|
48
|
+
if [[ -x "$dir/comment-checker" ]]; then
|
|
49
|
+
BINARY="$dir/comment-checker"
|
|
50
|
+
break
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Not found — silent skip
|
|
57
|
+
if [[ -z "$BINARY" ]]; then
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# --- Determine custom prompt ---
|
|
62
|
+
CUSTOM_PROMPT="$CFG_CUSTOM_PROMPT"
|
|
63
|
+
|
|
64
|
+
# --- Read stdin (PostToolUse hook input) ---
|
|
65
|
+
INPUT=$(cat)
|
|
66
|
+
|
|
67
|
+
# --- Build command args ---
|
|
68
|
+
CMD_ARGS=("$BINARY" "check")
|
|
69
|
+
if [[ -n "$CUSTOM_PROMPT" ]]; then
|
|
70
|
+
CMD_ARGS+=("--prompt" "$CUSTOM_PROMPT")
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# --- Run binary and handle output ---
|
|
74
|
+
STDERR_FILE=$(mktemp)
|
|
75
|
+
trap 'rm -f "$STDERR_FILE"' EXIT
|
|
76
|
+
|
|
77
|
+
EXIT_CODE=0
|
|
78
|
+
echo "$INPUT" | "${CMD_ARGS[@]}" 2>"$STDERR_FILE" || EXIT_CODE=$?
|
|
79
|
+
|
|
80
|
+
if [[ $EXIT_CODE -eq 2 ]]; then
|
|
81
|
+
# Comments detected — read warning from stderr
|
|
82
|
+
WARNING=$(cat "$STDERR_FILE")
|
|
83
|
+
if [[ -n "$WARNING" ]]; then
|
|
84
|
+
# Escape for JSON: backslashes, quotes, newlines, tabs
|
|
85
|
+
WARNING=$(echo "$WARNING" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
|
|
86
|
+
echo "{\"systemMessage\":\"$WARNING\"}"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Always exit 0
|
|
91
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@engineereddev/fractal-planner",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Iterative planning and execution framework for Claude Code with fractal task decomposition and builder/verifier agent teams",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "EngineeredDev",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"!dist/src/__tests__",
|
|
13
|
+
"!dist/**/*.d.ts.map",
|
|
14
|
+
"skills",
|
|
15
|
+
"hooks",
|
|
16
|
+
"agents",
|
|
17
|
+
".claude-plugin"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"plugin",
|
|
23
|
+
"agent-teams",
|
|
24
|
+
"planning",
|
|
25
|
+
"fractal",
|
|
26
|
+
"task-decomposition"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/EngineeredDev/fractal-planner.git"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun build ./src/index.ts --outdir dist --target node --format esm && bun build ./src/cli/classify-intent.ts ./src/cli/generate-plan.ts ./src/cli/validate-tasks.ts ./src/cli/load-config.ts ./src/cli/compute-signals.ts --outdir dist/cli --target node --format esm && tsc --emitDeclarationOnly",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"clean": "rm -rf dist && find src skills -type f \\( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' -o -name '*.d.ts.map' \\) -delete",
|
|
36
|
+
"lint": "oxlint",
|
|
37
|
+
"lint:fix": "oxlint --fix --fix-suggestions",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"test:watch": "bun test --watch",
|
|
41
|
+
"test:coverage": "bun test --coverage",
|
|
42
|
+
"prepublishOnly": "bun run clean && bun run build",
|
|
43
|
+
"release": "bash scripts/release.sh"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.39",
|
|
47
|
+
"zod": "^4.3.6"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/bun": "^1.3.9",
|
|
51
|
+
"@types/node": "^25.2.3",
|
|
52
|
+
"oxlint": "^1.48.0",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"optionalDependencies": {
|
|
57
|
+
"@code-yeongyu/comment-checker": "^0.7.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
},
|
|
62
|
+
"trustedDependencies": [
|
|
63
|
+
"@code-yeongyu/comment-checker"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fp:commit
|
|
3
|
+
description: Create git commits with style detection for fractal-planner tasks
|
|
4
|
+
context: fork
|
|
5
|
+
agent: general-purpose
|
|
6
|
+
allowed-tools: Bash, Read, Grep
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Git Commit Agent
|
|
10
|
+
|
|
11
|
+
You are a git commit specialist spawned by the fractal-planner implementation lead.
|
|
12
|
+
|
|
13
|
+
## Your Task
|
|
14
|
+
|
|
15
|
+
Create ONE atomic git commit for a completed implementation task.
|
|
16
|
+
|
|
17
|
+
## Process
|
|
18
|
+
|
|
19
|
+
### Step 1: Detect Commit Style (MANDATORY)
|
|
20
|
+
|
|
21
|
+
Run these commands in parallel:
|
|
22
|
+
```bash
|
|
23
|
+
git log -30 --pretty=format:"%s"
|
|
24
|
+
git branch --show-current
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Analyze the output:
|
|
28
|
+
- Count semantic commits (pattern: `^(feat|fix|chore|refactor|docs|test|ci|style|perf|build)(\(.+\))?:`)
|
|
29
|
+
- Count Korean vs English commits (detect Hangul characters using unicode ranges: [\uAC00-\uD7A3])
|
|
30
|
+
- Determine STYLE and LANGUAGE
|
|
31
|
+
|
|
32
|
+
Output this result (MANDATORY):
|
|
33
|
+
```
|
|
34
|
+
STYLE DETECTION RESULT
|
|
35
|
+
======================
|
|
36
|
+
Analyzed: 30 commits
|
|
37
|
+
|
|
38
|
+
Language: [KOREAN | ENGLISH]
|
|
39
|
+
- Korean: N commits (X%)
|
|
40
|
+
- English: M commits (Y%)
|
|
41
|
+
|
|
42
|
+
Style: [SEMANTIC | PLAIN | SHORT]
|
|
43
|
+
- Semantic (feat:, fix:): N (X%)
|
|
44
|
+
- Plain: M (Y%)
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
1. "<actual commit from log>"
|
|
48
|
+
2. "<actual commit from log>"
|
|
49
|
+
|
|
50
|
+
Using: [LANGUAGE] + [STYLE]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Style detection rules**:
|
|
54
|
+
- If semantic commits ≥ 50% → STYLE = SEMANTIC
|
|
55
|
+
- Else if commits with ≤3 words ≥ 33% → STYLE = SHORT
|
|
56
|
+
- Else → STYLE = PLAIN
|
|
57
|
+
|
|
58
|
+
**Language detection rules**:
|
|
59
|
+
- If Korean commits ≥ 50% → LANGUAGE = KOREAN
|
|
60
|
+
- Else → LANGUAGE = ENGLISH
|
|
61
|
+
|
|
62
|
+
**Edge cases**:
|
|
63
|
+
- Empty repo (no commits) → default to PLAIN + ENGLISH
|
|
64
|
+
- Can't run git log → default to PLAIN + ENGLISH
|
|
65
|
+
|
|
66
|
+
### Step 2: Generate Commit Message
|
|
67
|
+
|
|
68
|
+
Transform task description based on detected style:
|
|
69
|
+
|
|
70
|
+
**SEMANTIC + ENGLISH**:
|
|
71
|
+
- "Add X" → "feat: add X"
|
|
72
|
+
- "Fix X" → "fix: X"
|
|
73
|
+
- "Update X" → "chore: update X"
|
|
74
|
+
- "Refactor X" → "refactor: X"
|
|
75
|
+
- "Remove X" → "chore: remove X"
|
|
76
|
+
- "Test X" → "test: X"
|
|
77
|
+
- Default → "chore: {task description}"
|
|
78
|
+
|
|
79
|
+
**PLAIN + ENGLISH**:
|
|
80
|
+
- Use task description as-is, capitalize first letter
|
|
81
|
+
- "add user auth" → "Add user auth"
|
|
82
|
+
|
|
83
|
+
**SEMANTIC + KOREAN**:
|
|
84
|
+
- "Add X" → "feat: X 추가"
|
|
85
|
+
- "Fix X" → "fix: X 수정"
|
|
86
|
+
- Follow repo patterns from git log examples
|
|
87
|
+
|
|
88
|
+
**SHORT**:
|
|
89
|
+
- Extract 1-2 keywords from task description
|
|
90
|
+
- "Add user authentication" → "user auth"
|
|
91
|
+
|
|
92
|
+
### Step 3: Create Commit
|
|
93
|
+
|
|
94
|
+
Stage ONLY the reported files (not all changes):
|
|
95
|
+
```bash
|
|
96
|
+
# Stage each file explicitly
|
|
97
|
+
git add /path/to/file1.ts
|
|
98
|
+
git add /path/to/file2.test.ts
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
# Verify what's staged
|
|
102
|
+
git diff --staged --stat
|
|
103
|
+
|
|
104
|
+
# Commit with generated message
|
|
105
|
+
git commit -m "<generated message>"
|
|
106
|
+
|
|
107
|
+
# Capture commit hash
|
|
108
|
+
git log -1 --format="%h %s"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Step 4: Report Result
|
|
112
|
+
|
|
113
|
+
Message the lead with:
|
|
114
|
+
```
|
|
115
|
+
COMMIT COMPLETED
|
|
116
|
+
Task: {id}
|
|
117
|
+
Hash: abc1234
|
|
118
|
+
|
|
119
|
+
Details:
|
|
120
|
+
abc1234 - <commit message>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**On error**, message:
|
|
124
|
+
```
|
|
125
|
+
COMMIT FAILED
|
|
126
|
+
Task: {id}
|
|
127
|
+
Error: <error message>
|
|
128
|
+
Files were not committed.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Error Handling
|
|
132
|
+
|
|
133
|
+
**Scenario: Nothing to commit**
|
|
134
|
+
- If `git commit` reports "nothing to commit"
|
|
135
|
+
- Message lead: "COMMIT SKIPPED (no changes to commit)"
|
|
136
|
+
- This is NOT an error
|
|
137
|
+
|
|
138
|
+
**Scenario: Dirty working tree**
|
|
139
|
+
- If other unstaged changes exist beyond the reported files
|
|
140
|
+
- Commit ONLY the reported files (git add them explicitly)
|
|
141
|
+
- Do NOT stage unrelated changes
|
|
142
|
+
|
|
143
|
+
**Scenario: Git not available**
|
|
144
|
+
- If `git` command not found
|
|
145
|
+
- Message lead: "COMMIT SKIPPED (git not found)"
|
|
146
|
+
|
|
147
|
+
**Scenario: Commit fails (merge conflict, permissions, etc)**
|
|
148
|
+
- Report exact error message to lead
|
|
149
|
+
- Use "COMMIT FAILED" format above
|
|
150
|
+
|
|
151
|
+
## Important Rules
|
|
152
|
+
|
|
153
|
+
- NEVER commit files not in the FILES_MODIFIED list
|
|
154
|
+
- NEVER run `git add .` or `git add -A` (stage files explicitly)
|
|
155
|
+
- ALWAYS verify staging with `git diff --staged --stat` before committing
|
|
156
|
+
- Report exact commit hash (7-char short hash) back to lead
|
|
157
|
+
- Keep commit message concise (under 72 characters for subject line)
|