@vibe-x/agent-better-checkpoint 0.3.2 → 0.3.4

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/bin/cli.mjs CHANGED
@@ -38,7 +38,7 @@ const CONFIG_TEMPLATE = join(PLATFORM_DIR, 'config.template.yml');
38
38
  // ============================================================
39
39
 
40
40
  function parseArgs(argv) {
41
- const args = { platform: null, uninstall: false, target: null };
41
+ const args = { platform: null, uninstall: false, target: null, activate: false };
42
42
  for (let i = 2; i < argv.length; i++) {
43
43
  switch (argv[i]) {
44
44
  case '--platform':
@@ -58,6 +58,9 @@ function parseArgs(argv) {
58
58
  case '--uninstall':
59
59
  args.uninstall = true;
60
60
  break;
61
+ case '--activate':
62
+ args.activate = true;
63
+ break;
61
64
  case '--help':
62
65
  case '-h':
63
66
  printHelp();
@@ -82,6 +85,7 @@ Usage:
82
85
  Options:
83
86
  --platform <cursor|claude> Target AI platform (auto-detected if omitted)
84
87
  --target <path> Project-only install (no global). Use . for cwd
88
+ --activate Activate checkpoint rules in current project (AGENTS.md only)
85
89
  --uninstall Remove installed files and hook registrations
86
90
  -h, --help Show this help message
87
91
  `);
@@ -142,6 +146,80 @@ function writeJsonFile(filepath, data) {
142
146
  writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
143
147
  }
144
148
 
149
+ // ============================================================
150
+ // AGENTS.md block management
151
+ // ============================================================
152
+
153
+ const AGENTS_BLOCK_START = '<!-- [ABC:agent-better-checkpoint:start] -->';
154
+ const AGENTS_BLOCK_END = '<!-- [ABC:agent-better-checkpoint:end] -->';
155
+
156
+ const AGENTS_BLOCK_CONTENT = `${AGENTS_BLOCK_START}
157
+ ## Checkpoint Commit Rule
158
+
159
+ After completing any file edit (write_to_file, replace_in_file, multi_replace_in_file, or similar),
160
+ you MUST:
161
+
162
+ 1. Load the "agent-better-checkpoint" skill (if not already loaded)
163
+ 2. Follow its instructions to create a checkpoint commit
164
+ 3. Do NOT proceed to the next task until the commit is done
165
+
166
+ This ensures every meaningful code change is tracked with a semantic Git commit.
167
+ ${AGENTS_BLOCK_END}`;
168
+
169
+ function injectAgentsMdBlock(targetDir) {
170
+ const agentsMdPath = join(targetDir, 'AGENTS.md');
171
+ let content = '';
172
+
173
+ if (existsSync(agentsMdPath)) {
174
+ content = readFileSync(agentsMdPath, 'utf-8');
175
+ // Check if block already exists
176
+ if (content.includes(AGENTS_BLOCK_START)) {
177
+ console.log(` AGENTS.md → block already exists (skipped)`);
178
+ return;
179
+ }
180
+ }
181
+
182
+ // Append block to end of file (or create new file)
183
+ const newContent = content
184
+ ? content.trimEnd() + '\n\n' + AGENTS_BLOCK_CONTENT + '\n'
185
+ : AGENTS_BLOCK_CONTENT + '\n';
186
+
187
+ writeFileSync(agentsMdPath, newContent, 'utf-8');
188
+ console.log(` AGENTS.md → ${agentsMdPath}`);
189
+ }
190
+
191
+ function removeAgentsMdBlock(targetDir) {
192
+ const agentsMdPath = join(targetDir, 'AGENTS.md');
193
+
194
+ if (!existsSync(agentsMdPath)) {
195
+ return;
196
+ }
197
+
198
+ const content = readFileSync(agentsMdPath, 'utf-8');
199
+
200
+ if (!content.includes(AGENTS_BLOCK_START)) {
201
+ return;
202
+ }
203
+
204
+ // Remove the block (including surrounding newlines)
205
+ const blockRegex = new RegExp(
206
+ `\\n*${AGENTS_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n*`,
207
+ 'g'
208
+ );
209
+
210
+ let newContent = content.replace(blockRegex, '\n');
211
+ newContent = newContent.trim();
212
+
213
+ if (newContent === '') {
214
+ // If file is empty after removal, delete it
215
+ rmSync(agentsMdPath, { force: true });
216
+ console.log(` Removed ${agentsMdPath} (empty after cleanup)`);
217
+ } else {
218
+ writeFileSync(agentsMdPath, newContent + '\n', 'utf-8');
219
+ console.log(` Cleaned ${agentsMdPath}`);
220
+ }
221
+ }
222
+
145
223
  // ============================================================
146
224
  // Install logic
147
225
  // ============================================================
@@ -155,11 +233,14 @@ function installScripts(osType) {
155
233
 
156
234
  // 双端脚本都安装,方便跨平台使用
157
235
  copyFileSafe(join(PLATFORM_DIR, 'unix', 'checkpoint.sh'), join(scriptsDir, 'checkpoint.sh'));
236
+ copyFileSafe(join(PLATFORM_DIR, 'unix', 'alloc_patch.sh'), join(scriptsDir, 'alloc_patch.sh'));
158
237
  copyFileSafe(join(PLATFORM_DIR, 'unix', 'check_uncommitted.sh'), join(hooksDir, 'check_uncommitted.sh'));
159
238
  setExecutable(join(scriptsDir, 'checkpoint.sh'));
239
+ setExecutable(join(scriptsDir, 'alloc_patch.sh'));
160
240
  setExecutable(join(hooksDir, 'check_uncommitted.sh'));
161
241
 
162
242
  copyFileSafe(join(PLATFORM_DIR, 'win', 'checkpoint.ps1'), join(scriptsDir, 'checkpoint.ps1'));
243
+ copyFileSafe(join(PLATFORM_DIR, 'win', 'alloc_patch.ps1'), join(scriptsDir, 'alloc_patch.ps1'));
163
244
  copyFileSafe(join(PLATFORM_DIR, 'win', 'check_uncommitted.ps1'), join(hooksDir, 'check_uncommitted.ps1'));
164
245
 
165
246
  console.log(` Scripts → ${scriptsDir}/`);
@@ -227,12 +308,18 @@ function registerCursorHook(osType) {
227
308
  function installProjectOnly(targetDir, aiPlatform, osType) {
228
309
  const root = resolve(targetDir);
229
310
 
230
- // .vibe-x/agent-better-checkpoint: checkpoint 脚本 + config
311
+ // .vibe-x/agent-better-checkpoint: checkpoint 脚本 + helper + config
231
312
  const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
232
313
  ensureDir(vibeXBase);
233
314
  copyFileSafe(join(PLATFORM_DIR, 'unix', 'checkpoint.sh'), join(vibeXBase, 'checkpoint.sh'));
315
+ copyFileSafe(join(PLATFORM_DIR, 'unix', 'alloc_patch.sh'), join(vibeXBase, 'alloc_patch.sh'));
316
+ copyFileSafe(join(PLATFORM_DIR, 'unix', 'check_uncommitted.sh'), join(vibeXBase, 'check_uncommitted.sh'));
234
317
  copyFileSafe(join(PLATFORM_DIR, 'win', 'checkpoint.ps1'), join(vibeXBase, 'checkpoint.ps1'));
318
+ copyFileSafe(join(PLATFORM_DIR, 'win', 'alloc_patch.ps1'), join(vibeXBase, 'alloc_patch.ps1'));
319
+ copyFileSafe(join(PLATFORM_DIR, 'win', 'check_uncommitted.ps1'), join(vibeXBase, 'check_uncommitted.ps1'));
235
320
  setExecutable(join(vibeXBase, 'checkpoint.sh'));
321
+ setExecutable(join(vibeXBase, 'alloc_patch.sh'));
322
+ setExecutable(join(vibeXBase, 'check_uncommitted.sh'));
236
323
  const configDest = join(vibeXBase, 'config.yml');
237
324
  if (!existsSync(configDest) && existsSync(CONFIG_TEMPLATE)) {
238
325
  copyFileSafe(CONFIG_TEMPLATE, configDest);
@@ -271,6 +358,9 @@ function installProjectOnly(targetDir, aiPlatform, osType) {
271
358
  // Claude Code: settings.json 为全局,无项目级 hooks,仅安装 skill 和脚本
272
359
  console.log(` Hooks → (Claude stop hook is global-only, skipped for project install)`);
273
360
  }
361
+
362
+ // Inject AGENTS.md block (project-only)
363
+ injectAgentsMdBlock(root);
274
364
  }
275
365
 
276
366
  function uninstallProjectOnly(targetDir, aiPlatform) {
@@ -279,9 +369,22 @@ function uninstallProjectOnly(targetDir, aiPlatform) {
279
369
  const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
280
370
  const skillRoot = aiPlatform === 'cursor' ? '.cursor' : '.claude';
281
371
  const skillDir = join(root, skillRoot, 'skills', SKILL_NAME);
282
- if (existsSync(vibeXBase)) {
283
- rmSync(vibeXBase, { recursive: true, force: true });
284
- console.log(` Removed ${vibeXBase}`);
372
+ const checkpointShPath = join(vibeXBase, 'checkpoint.sh');
373
+ const checkpointPs1Path = join(vibeXBase, 'checkpoint.ps1');
374
+ const allocPatchShPath = join(vibeXBase, 'alloc_patch.sh');
375
+ const allocPatchPs1Path = join(vibeXBase, 'alloc_patch.ps1');
376
+ const checkUncommittedShPath = join(vibeXBase, 'check_uncommitted.sh');
377
+ const checkUncommittedPs1Path = join(vibeXBase, 'check_uncommitted.ps1');
378
+ for (const filePath of [
379
+ checkpointShPath,
380
+ checkpointPs1Path,
381
+ allocPatchShPath,
382
+ allocPatchPs1Path,
383
+ checkUncommittedShPath,
384
+ checkUncommittedPs1Path,
385
+ join(vibeXBase, 'config.yml'),
386
+ ]) {
387
+ if (existsSync(filePath)) rmSync(filePath, { force: true });
285
388
  }
286
389
 
287
390
  if (existsSync(skillDir)) {
@@ -326,6 +429,9 @@ function uninstallProjectOnly(targetDir, aiPlatform) {
326
429
  rmSync(hooksDir, { recursive: true, force: true });
327
430
  }
328
431
  }
432
+
433
+ // Remove AGENTS.md block
434
+ removeAgentsMdBlock(root);
329
435
  }
330
436
 
331
437
  function registerClaudeHook(osType) {
@@ -423,6 +529,123 @@ function unregisterClaudeHook() {
423
529
  console.log(` Cleaned config: ${settingsPath}`);
424
530
  }
425
531
 
532
+ // ============================================================
533
+ // Activate logic (AGENTS.md only, with installation check)
534
+ // ============================================================
535
+
536
+ const SUPPORTED_PLATFORMS = ['cursor', 'claude'];
537
+
538
+ function checkGlobalInstallation() {
539
+ const home = homedir();
540
+ const results = {
541
+ hasScripts: existsSync(join(INSTALL_BASE, 'scripts', 'checkpoint.sh')) ||
542
+ existsSync(join(INSTALL_BASE, 'scripts', 'checkpoint.ps1')),
543
+ platforms: {}
544
+ };
545
+
546
+ for (const p of SUPPORTED_PLATFORMS) {
547
+ const skillDir = p === 'cursor' ? '.cursor' : '.claude';
548
+ results.platforms[p] = {
549
+ hasSkill: existsSync(join(home, skillDir, 'skills', SKILL_NAME, 'SKILL.md'))
550
+ };
551
+ }
552
+
553
+ results.hasAnySkill = SUPPORTED_PLATFORMS.some(p => results.platforms[p].hasSkill);
554
+ results.isFullyInstalled = results.hasScripts && results.hasAnySkill;
555
+ return results;
556
+ }
557
+
558
+ function checkProjectInstallation(targetDir) {
559
+ const root = resolve(targetDir);
560
+ const results = {
561
+ hasScripts: existsSync(join(root, '.vibe-x', 'agent-better-checkpoint', 'checkpoint.sh')) ||
562
+ existsSync(join(root, '.vibe-x', 'agent-better-checkpoint', 'checkpoint.ps1')),
563
+ platforms: {}
564
+ };
565
+
566
+ for (const p of SUPPORTED_PLATFORMS) {
567
+ const skillDir = p === 'cursor' ? '.cursor' : '.claude';
568
+ results.platforms[p] = {
569
+ hasSkill: existsSync(join(root, skillDir, 'skills', SKILL_NAME, 'SKILL.md'))
570
+ };
571
+ }
572
+
573
+ results.hasAnySkill = SUPPORTED_PLATFORMS.some(p => results.platforms[p].hasSkill);
574
+ results.isFullyInstalled = results.hasScripts && results.hasAnySkill;
575
+ return results;
576
+ }
577
+
578
+ function activateProject(targetDir) {
579
+ const root = resolve(targetDir);
580
+
581
+ // Check if already has AGENTS.md block
582
+ const agentsMdPath = join(root, 'AGENTS.md');
583
+ if (existsSync(agentsMdPath)) {
584
+ const content = readFileSync(agentsMdPath, 'utf-8');
585
+ if (content.includes(AGENTS_BLOCK_START)) {
586
+ console.log(`\n⚠️ AGENTS.md already contains checkpoint rules. Nothing to do.`);
587
+ return;
588
+ }
589
+ }
590
+
591
+ // Step 1: Check project-level installation (all platforms)
592
+ const projectStatus = checkProjectInstallation(root);
593
+ if (projectStatus.isFullyInstalled) {
594
+ // Project has both scripts and skill for at least one platform
595
+ console.log(`\n[Activate] Adding checkpoint rules to ${root}...`);
596
+ injectAgentsMdBlock(root);
597
+ console.log(`\n✅ Activation complete!`);
598
+ console.log(`\nThe AI agent will now follow checkpoint commit rules in this project.`);
599
+ return;
600
+ }
601
+
602
+ // Step 2: Check global installation (all platforms)
603
+ const globalStatus = checkGlobalInstallation();
604
+ if (globalStatus.isFullyInstalled) {
605
+ // Global has both scripts and skill for at least one platform
606
+ console.log(`\n[Activate] Adding checkpoint rules to ${root}...`);
607
+ injectAgentsMdBlock(root);
608
+ console.log(`\n✅ Activation complete!`);
609
+ console.log(`\nThe AI agent will now follow checkpoint commit rules in this project.`);
610
+ return;
611
+ }
612
+
613
+ // Step 3: Neither project nor global is fully installed - provide detailed diagnosis
614
+ const hasAnyProjectSkill = projectStatus.hasAnySkill;
615
+ const hasAnyGlobalSkill = globalStatus.hasAnySkill;
616
+ const hasAnyProjectScripts = projectStatus.hasScripts;
617
+ const hasAnyGlobalScripts = globalStatus.hasScripts;
618
+
619
+ const hasAnySkill = hasAnyProjectSkill || hasAnyGlobalSkill;
620
+ const hasAnyScripts = hasAnyProjectScripts || hasAnyGlobalScripts;
621
+
622
+ if (!hasAnySkill && !hasAnyScripts) {
623
+ console.log(`\n⚠️ No agent-better-checkpoint installation detected.`);
624
+ console.log(`\nChecked locations:`);
625
+ console.log(` Project: ${root}`);
626
+ console.log(` Global: ${INSTALL_BASE}`);
627
+ console.log(`\nPlease install first:`);
628
+ console.log(` Global: npx @vibe-x/agent-better-checkpoint`);
629
+ console.log(` Project-only: npx @vibe-x/agent-better-checkpoint --target . --platform cursor|claude`);
630
+ console.log(`\nThen run --activate again.`);
631
+ process.exit(1);
632
+ }
633
+
634
+ if (!hasAnySkill) {
635
+ console.log(`\n⚠️ Checkpoint scripts found, but skill (SKILL.md) is missing.`);
636
+ console.log(`\nThe AI agent needs the skill to know how to commit. Please run:`);
637
+ console.log(` npx @vibe-x/agent-better-checkpoint --platform cursor|claude`);
638
+ process.exit(1);
639
+ }
640
+
641
+ if (!hasAnyScripts) {
642
+ console.log(`\n⚠️ Skill found, but checkpoint scripts are missing.`);
643
+ console.log(`\nPlease reinstall to get the scripts:`);
644
+ console.log(` npx @vibe-x/agent-better-checkpoint`);
645
+ process.exit(1);
646
+ }
647
+ }
648
+
426
649
  // ============================================================
427
650
  // Main entry
428
651
  // ============================================================
@@ -433,7 +656,7 @@ function main() {
433
656
  const aiPlatform = args.platform || detectAIPlatform();
434
657
  const projectTargetDir = args.target ? resolve(args.target) : null;
435
658
 
436
- if (!aiPlatform && !projectTargetDir && !args.uninstall) {
659
+ if (!aiPlatform && !projectTargetDir && !args.uninstall && !args.activate) {
437
660
  console.error(
438
661
  'Error: could not detect AI platform.\n' +
439
662
  'Please specify: npx @vibe-x/agent-better-checkpoint --platform cursor|claude'
@@ -476,6 +699,10 @@ function main() {
476
699
  if (platforms.length === 0) console.log('\nNo global installation found.');
477
700
  }
478
701
  console.log('\n✅ Uninstallation complete!');
702
+ } else if (args.activate) {
703
+ // Activate: only inject AGENTS.md, check installation first
704
+ const targetDir = projectTargetDir || process.cwd();
705
+ activateProject(targetDir);
479
706
  } else {
480
707
  if (projectTargetDir) {
481
708
  if (!aiPlatform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-x/agent-better-checkpoint",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Semantic Git checkpoint commits for AI coding sessions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,8 +10,14 @@
10
10
  "bin/",
11
11
  "platform/",
12
12
  "skill/",
13
- "LICENSE"
13
+ "LICENSE",
14
+ "scripts/"
14
15
  ],
16
+ "scripts": {
17
+ "release:check": "node ./scripts/release-check.mjs",
18
+ "preversion": "npm run release:check",
19
+ "prepublishOnly": "npm run release:check"
20
+ },
15
21
  "keywords": [
16
22
  "ai",
17
23
  "agent",
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # alloc_patch.sh — Allocate a temporary patch file for hunk-level checkpoint commits
4
+ #
5
+ # Creates a writable patch path under ~/.vibe-x/agent-better-checkpoint/tmp/
6
+ # and returns a one-line JSON payload with id, createdAt, path, and ttlHours.
7
+ #
8
+ # Usage:
9
+ # alloc_patch.sh --workspace /path/to/repo
10
+ # alloc_patch.sh --help
11
+
12
+ set -euo pipefail
13
+
14
+ TTL_HOURS=48
15
+ TMP_ROOT="${HOME}/.vibe-x/agent-better-checkpoint/tmp"
16
+ WORKSPACE=""
17
+
18
+ usage() {
19
+ cat <<'EOF'
20
+ alloc_patch.sh — Allocate a temporary patch file for checkpoint hunks
21
+
22
+ Usage:
23
+ alloc_patch.sh --workspace <path>
24
+ alloc_patch.sh --help
25
+
26
+ Options:
27
+ --workspace <path> Workspace root used to derive a stable temp namespace
28
+ --help Show this help message
29
+ EOF
30
+ }
31
+
32
+ error() {
33
+ local code="$1"
34
+ shift
35
+ echo "${code}: $*" >&2
36
+ exit 1
37
+ }
38
+
39
+ json_escape() {
40
+ local str="$1"
41
+ str="${str//\\/\\\\}"
42
+ str="${str//\"/\\\"}"
43
+ str="${str//$'\n'/\\n}"
44
+ str="${str//$'\r'/\\r}"
45
+ str="${str//$'\t'/\\t}"
46
+ printf '%s' "$str"
47
+ }
48
+
49
+ workspace_hash() {
50
+ local input="$1"
51
+ if command -v shasum >/dev/null 2>&1; then
52
+ printf '%s' "$input" | shasum -a 256 | awk '{print substr($1,1,8)}'
53
+ elif command -v sha256sum >/dev/null 2>&1; then
54
+ printf '%s' "$input" | sha256sum | awk '{print substr($1,1,8)}'
55
+ else
56
+ printf '%s' "$input" | cksum | awk '{print $1}'
57
+ fi
58
+ }
59
+
60
+ random_id() {
61
+ local id
62
+ id=$(LC_ALL=C tr -dc 'A-Z0-9' < /dev/urandom | head -c 10 || true)
63
+ if [[ -z "$id" ]]; then
64
+ id="$(date -u +%s)"
65
+ fi
66
+ printf '%s' "$id"
67
+ }
68
+
69
+ cleanup_expired() {
70
+ local root="$1"
71
+ [[ -d "$root" ]] || return 0
72
+
73
+ find "$root" -type f -name 'patch-*.patch' -mtime +1 -exec rm -f {} + 2>/dev/null || true
74
+ find "$root" -type d -empty -mindepth 1 -exec rmdir {} + 2>/dev/null || true
75
+ }
76
+
77
+ while [[ $# -gt 0 ]]; do
78
+ case "$1" in
79
+ --workspace)
80
+ WORKSPACE="${2:-}"
81
+ shift 2
82
+ ;;
83
+ --help|-h)
84
+ usage
85
+ exit 0
86
+ ;;
87
+ *)
88
+ error "ABC_TEMP_ALLOC_FAILED" "Unknown option: $1"
89
+ ;;
90
+ esac
91
+ done
92
+
93
+ if [[ -z "$WORKSPACE" ]]; then
94
+ error "ABC_TEMP_ALLOC_FAILED" "Missing required --workspace argument. Please provide the workspace root path."
95
+ fi
96
+
97
+ if ! mkdir -p "$TMP_ROOT" 2>/dev/null; then
98
+ error "ABC_TEMP_ALLOC_FAILED" "Failed to create the temp root at ${TMP_ROOT}. Please check directory permissions."
99
+ fi
100
+
101
+ cleanup_expired "$TMP_ROOT"
102
+
103
+ WORKSPACE_HASH=$(workspace_hash "$WORKSPACE")
104
+ WORKSPACE_DIR="${TMP_ROOT}/${WORKSPACE_HASH}"
105
+ if ! mkdir -p "$WORKSPACE_DIR" 2>/dev/null; then
106
+ error "ABC_TEMP_ALLOC_FAILED" "Failed to create the workspace temp directory at ${WORKSPACE_DIR}. Please check directory permissions."
107
+ fi
108
+
109
+ STAMP=$(date -u +%Y%m%dT%H%M%SZ)
110
+ CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
111
+ ID=$(random_id)
112
+ PATCH_PATH="${WORKSPACE_DIR}/patch-${STAMP}-${ID}.patch"
113
+
114
+ if ! : > "$PATCH_PATH" 2>/dev/null; then
115
+ error "ABC_TEMP_ALLOC_FAILED" "Failed to create a temporary patch file at ${PATCH_PATH}. Please check directory permissions and available disk space."
116
+ fi
117
+
118
+ printf '{"ok":true,"id":"%s","createdAt":"%s","path":"%s","ttlHours":%s}\n' \
119
+ "$(json_escape "$ID")" \
120
+ "$(json_escape "$CREATED_AT")" \
121
+ "$(json_escape "$PATCH_PATH")" \
122
+ "$TTL_HOURS"
@@ -427,7 +427,7 @@ build_and_output_reminder() {
427
427
  changes_indented=$(echo "$changes" | sed 's/^/ /')
428
428
 
429
429
  # Project-local script; fallback to global
430
- local checkpoint_cmd_sh checkpoint_cmd_ps1
430
+ local checkpoint_cmd_sh checkpoint_cmd_ps1 alloc_patch_cmd_sh alloc_patch_cmd_ps1
431
431
  if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/checkpoint.sh" ]]; then
432
432
  checkpoint_cmd_sh=".vibe-x/agent-better-checkpoint/checkpoint.sh"
433
433
  else
@@ -438,6 +438,16 @@ build_and_output_reminder() {
438
438
  else
439
439
  checkpoint_cmd_ps1='\$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/checkpoint.ps1'
440
440
  fi
441
+ if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/alloc_patch.sh" ]]; then
442
+ alloc_patch_cmd_sh=".vibe-x/agent-better-checkpoint/alloc_patch.sh"
443
+ else
444
+ alloc_patch_cmd_sh="~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh"
445
+ fi
446
+ if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/alloc_patch.ps1" ]]; then
447
+ alloc_patch_cmd_ps1='.\\.vibe-x\\agent-better-checkpoint\\alloc_patch.ps1'
448
+ else
449
+ alloc_patch_cmd_ps1='\$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.ps1'
450
+ fi
441
451
 
442
452
  local reminder
443
453
  reminder="## ⚠️ Uncommitted Changes Detected
@@ -449,17 +459,24 @@ There are uncommitted changes in the workspace. Please create a checkpoint commi
449
459
  ${changes_indented}
450
460
  \`\`\`
451
461
 
452
- **Action Required**: Run the checkpoint script to commit these changes:
462
+ **Action Required**: Allocate a patch file, write only the selected hunks for this conversation, then run the checkpoint script.
453
463
 
454
464
  **macOS/Linux:**
455
465
  \`\`\`bash
456
- ${checkpoint_cmd_sh} \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback
466
+ PATCH_JSON=\$(${alloc_patch_cmd_sh} --workspace \"${workspace}\")
467
+ PATCH_PATH=\$(printf '%s' \"\$PATCH_JSON\" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
468
+ # Write only the selected hunks for this conversation to \"\$PATCH_PATH\" as a unified diff patch.
469
+ ${checkpoint_cmd_sh} \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback --patch-file \"\$PATCH_PATH\"
457
470
  \`\`\`
458
471
 
459
472
  **Windows (PowerShell):**
460
473
  \`\`\`powershell
461
- powershell -File \"${checkpoint_cmd_ps1}\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback
462
- \`\`\`"
474
+ \$patch = powershell -File \"${alloc_patch_cmd_ps1}\" -Workspace \"${workspace}\" | ConvertFrom-Json
475
+ # Write only the selected hunks for this conversation to \$patch.path as a unified diff patch.
476
+ powershell -File \"${checkpoint_cmd_ps1}\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback -PatchFile \$patch.path
477
+ \`\`\`
478
+
479
+ If the checkpoint script returns an \`ABC_PATCH_*\` error, do not fall back to a full commit automatically. Explain the conflict and ask the user how to proceed."
463
480
 
464
481
  output_block "$reminder" "$platform"
465
482
  }
@@ -5,10 +5,10 @@
5
5
  # AI provides descriptive content (subject + body). This script:
6
6
  # 1. Truncates user-prompt (≤60 chars, head+tail)
7
7
  # 2. Appends metadata via git interpret-trailers
8
- # 3. Runs git add -A && git commit
8
+ # 3. Runs git add -A && git commit, or applies a selected patch to the index first
9
9
  #
10
10
  # Usage:
11
- # checkpoint.sh <message> [user-prompt] [--type auto|fallback]
11
+ # checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]
12
12
 
13
13
  set -euo pipefail
14
14
 
@@ -18,6 +18,7 @@ set -euo pipefail
18
18
  MESSAGE="${1:-}"
19
19
  USER_PROMPT="${2:-}"
20
20
  CHECKPOINT_TYPE="auto"
21
+ PATCH_FILE=""
21
22
 
22
23
  shift 2 2>/dev/null || true
23
24
  while [[ $# -gt 0 ]]; do
@@ -26,26 +27,53 @@ while [[ $# -gt 0 ]]; do
26
27
  CHECKPOINT_TYPE="${2:-auto}"
27
28
  shift 2
28
29
  ;;
30
+ --patch-file)
31
+ PATCH_FILE="${2:-}"
32
+ shift 2
33
+ ;;
34
+ --help|-h)
35
+ cat <<'EOF'
36
+ checkpoint.sh — Create semantic Git checkpoint commits
37
+
38
+ Usage:
39
+ checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]
40
+
41
+ Options:
42
+ --type <auto|fallback> Checkpoint type metadata
43
+ --patch-file <path> Apply only the selected patch hunks to the index before commit
44
+ --help Show this help message
45
+ EOF
46
+ exit 0
47
+ ;;
29
48
  *)
30
- shift
49
+ echo "Unknown option: $1" >&2
50
+ exit 1
31
51
  ;;
32
52
  esac
33
53
  done
34
54
 
35
55
  if [[ -z "$MESSAGE" ]]; then
36
56
  echo "Error: commit message is required" >&2
37
- echo "Usage: checkpoint.sh <message> [user-prompt] [--type auto|fallback]" >&2
57
+ echo "Usage: checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]" >&2
38
58
  exit 1
39
59
  fi
40
60
 
61
+ fail_checkpoint() {
62
+ local code="$1"
63
+ shift
64
+ echo "${code}: $*" >&2
65
+ exit 1
66
+ }
67
+
41
68
  # ============================================================
42
69
  # Platform detection
43
70
  # ============================================================
44
71
  detect_platform() {
45
- if [[ -n "${CLAUDE_CODE:-}" ]] || command -v claude &>/dev/null; then
46
- echo "claude-code"
47
- elif [[ -n "${CURSOR_VERSION:-}" ]] || [[ -n "${CURSOR_TRACE_ID:-}" ]]; then
72
+ # 优先检测运行时环境变量(谁在调用),而非安装态(command -v
73
+ if [[ -n "${CURSOR_AGENT:-}" ]] || [[ -n "${CURSOR_TRACE_ID:-}" ]] || [[ -n "${CURSOR_VERSION:-}" ]]; then
48
74
  echo "cursor"
75
+ elif [[ -n "${CLAUDE_CODE:-}" ]]; then
76
+ echo "claude-code"
49
77
  else
50
78
  echo "unknown"
51
79
  fi
@@ -59,10 +87,23 @@ AGENT_PLATFORM=$(detect_platform)
59
87
  truncate_prompt() {
60
88
  local prompt="$1"
61
89
  local max_len=60
90
+
91
+ # Ensure UTF-8 character-based (not byte-based) string operations.
92
+ # Without this, ${#prompt} and ${prompt:0:n} count bytes in C locale,
93
+ # which breaks multi-byte characters (e.g., Chinese) mid-character.
94
+ local old_lc_all="${LC_ALL:-}"
95
+ export LC_ALL=en_US.UTF-8
96
+
62
97
  local len=${#prompt}
63
98
 
64
99
  if [[ $len -le $max_len ]]; then
65
100
  echo "$prompt"
101
+ # Restore LC_ALL
102
+ if [[ -n "$old_lc_all" ]]; then
103
+ export LC_ALL="$old_lc_all"
104
+ else
105
+ unset LC_ALL
106
+ fi
66
107
  return
67
108
  fi
68
109
 
@@ -70,6 +111,14 @@ truncate_prompt() {
70
111
  local tail_len=$(( max_len - 3 - head_len ))
71
112
  local head="${prompt:0:$head_len}"
72
113
  local tail="${prompt:$((len - tail_len)):$tail_len}"
114
+
115
+ # Restore LC_ALL
116
+ if [[ -n "$old_lc_all" ]]; then
117
+ export LC_ALL="$old_lc_all"
118
+ else
119
+ unset LC_ALL
120
+ fi
121
+
73
122
  echo "${head}...${tail}"
74
123
  }
75
124
 
@@ -97,15 +146,44 @@ has_changes() {
97
146
  return 1
98
147
  }
99
148
 
100
- if ! has_changes; then
101
- echo "No changes to commit."
102
- exit 0
103
- fi
149
+ has_staged_changes() {
150
+ if ! git diff --cached --quiet 2>/dev/null; then
151
+ return 0
152
+ fi
153
+ return 1
154
+ }
104
155
 
105
- # ============================================================
106
- # git add -A
107
- # ============================================================
108
- git add -A
156
+ if [[ -z "$PATCH_FILE" ]]; then
157
+ if ! has_changes; then
158
+ echo "No changes to commit."
159
+ exit 0
160
+ fi
161
+
162
+ # ============================================================
163
+ # git add -A
164
+ # ============================================================
165
+ git add -A
166
+ else
167
+ if [[ ! -f "$PATCH_FILE" ]]; then
168
+ fail_checkpoint "ABC_PATCH_FILE_MISSING" "Patch file not found at ${PATCH_FILE}. Please allocate and write the patch file before running checkpoint."
169
+ fi
170
+
171
+ if [[ ! -s "$PATCH_FILE" ]]; then
172
+ fail_checkpoint "ABC_PATCH_FILE_EMPTY" "Patch file at ${PATCH_FILE} is empty. Please write at least one selected hunk before running checkpoint."
173
+ fi
174
+
175
+ if ! git apply --cached --check "$PATCH_FILE" >/dev/null 2>&1; then
176
+ fail_checkpoint "ABC_PATCH_APPLY_FAILED" "Failed to apply the selected patch cleanly to the Git index. The target hunks may have drifted or overlap with other uncommitted edits."
177
+ fi
178
+
179
+ if ! git apply --cached "$PATCH_FILE" >/dev/null 2>&1; then
180
+ fail_checkpoint "ABC_PATCH_APPLY_FAILED" "Failed to apply the selected patch to the Git index after the dry run succeeded. Please verify the patch file and retry."
181
+ fi
182
+
183
+ if ! has_staged_changes; then
184
+ fail_checkpoint "ABC_PATCH_NO_STAGED_CHANGES" "The selected patch did not produce any staged changes. The target hunks may already be staged or no longer match the current repository state."
185
+ fi
186
+ fi
109
187
 
110
188
  # ============================================================
111
189
  # Build trailers and commit
@@ -119,6 +197,15 @@ if [[ -n "$TRUNCATED_PROMPT" ]]; then
119
197
  TRAILER_ARGS+=(--trailer "User-Prompt: ${TRUNCATED_PROMPT}")
120
198
  fi
121
199
 
122
- echo "$MESSAGE" | git interpret-trailers "${TRAILER_ARGS[@]}" | git commit -F -
200
+ if ! echo "$MESSAGE" | git interpret-trailers "${TRAILER_ARGS[@]}" | git commit -F -; then
201
+ if [[ -n "$PATCH_FILE" ]]; then
202
+ fail_checkpoint "ABC_PATCH_COMMIT_FAILED" "Git commit failed after staging the selected patch. Review the repository state and retry when ready."
203
+ fi
204
+ exit 1
205
+ fi
206
+
207
+ if [[ -n "$PATCH_FILE" ]]; then
208
+ rm -f "$PATCH_FILE"
209
+ fi
123
210
 
124
211
  echo "Checkpoint committed successfully."
@@ -0,0 +1,142 @@
1
+ <#
2
+ .SYNOPSIS
3
+ alloc_patch.ps1 — Allocate a temporary patch file for hunk-level checkpoint commits
4
+
5
+ .DESCRIPTION
6
+ Creates a writable patch path under ~/.vibe-x/agent-better-checkpoint/tmp/
7
+ and returns a one-line JSON payload with id, createdAt, path, and ttlHours.
8
+
9
+ .PARAMETER Workspace
10
+ Workspace root used to derive a stable temp namespace. Required.
11
+
12
+ .EXAMPLE
13
+ .\alloc_patch.ps1 -Workspace C:\repo
14
+ #>
15
+
16
+ param(
17
+ [string]$Workspace = "",
18
+ [switch]$Help
19
+ )
20
+
21
+ $ErrorActionPreference = "Stop"
22
+ $TTL_HOURS = 48
23
+ $TMP_ROOT = Join-Path $HOME ".vibe-x/agent-better-checkpoint/tmp"
24
+
25
+ function Show-Usage {
26
+ @"
27
+ alloc_patch.ps1 — Allocate a temporary patch file for checkpoint hunks
28
+
29
+ Usage:
30
+ alloc_patch.ps1 -Workspace <path>
31
+ alloc_patch.ps1 -Help
32
+
33
+ Options:
34
+ -Workspace <path> Workspace root used to derive a stable temp namespace
35
+ -Help Show this help message
36
+ "@ | Write-Output
37
+ }
38
+
39
+ function Fail-Alloc {
40
+ param(
41
+ [string]$Code,
42
+ [string]$Message
43
+ )
44
+
45
+ [Console]::Error.WriteLine("${Code}: ${Message}")
46
+ exit 1
47
+ }
48
+
49
+ function Get-WorkspaceHash {
50
+ param([string]$InputText)
51
+
52
+ $sha = [System.Security.Cryptography.SHA256]::Create()
53
+ try {
54
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($InputText)
55
+ $hashBytes = $sha.ComputeHash($bytes)
56
+ $hex = -join ($hashBytes | ForEach-Object { $_.ToString("x2") })
57
+ return $hex.Substring(0, 8)
58
+ }
59
+ finally {
60
+ $sha.Dispose()
61
+ }
62
+ }
63
+
64
+ function Get-RandomId {
65
+ $bytes = New-Object byte[] 6
66
+ [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
67
+ $hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })
68
+ return $hex.Substring(0, 10)
69
+ }
70
+
71
+ function Cleanup-Expired {
72
+ param([string]$Root)
73
+
74
+ if (-not (Test-Path $Root -PathType Container)) {
75
+ return
76
+ }
77
+
78
+ $cutoff = (Get-Date).ToUniversalTime().AddHours(-$TTL_HOURS)
79
+ Get-ChildItem -Path $Root -Recurse -File -Filter 'patch-*.patch' -ErrorAction SilentlyContinue |
80
+ Where-Object { $_.LastWriteTimeUtc -lt $cutoff } |
81
+ ForEach-Object {
82
+ Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
83
+ }
84
+
85
+ Get-ChildItem -Path $Root -Recurse -Directory -ErrorAction SilentlyContinue |
86
+ Sort-Object FullName -Descending |
87
+ ForEach-Object {
88
+ if (-not (Get-ChildItem -Path $_.FullName -Force -ErrorAction SilentlyContinue)) {
89
+ Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
90
+ }
91
+ }
92
+ }
93
+
94
+ if ($Help) {
95
+ Show-Usage
96
+ exit 0
97
+ }
98
+
99
+ if (-not $Workspace) {
100
+ Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Missing required -Workspace argument. Please provide the workspace root path."
101
+ }
102
+
103
+ try {
104
+ New-Item -ItemType Directory -Path $TMP_ROOT -Force | Out-Null
105
+ }
106
+ catch {
107
+ Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create the temp root at $TMP_ROOT. Please check directory permissions."
108
+ }
109
+
110
+ Cleanup-Expired -Root $TMP_ROOT
111
+
112
+ $workspaceHash = Get-WorkspaceHash -InputText $Workspace
113
+ $workspaceDir = Join-Path $TMP_ROOT $workspaceHash
114
+
115
+ try {
116
+ New-Item -ItemType Directory -Path $workspaceDir -Force | Out-Null
117
+ }
118
+ catch {
119
+ Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create the workspace temp directory at $workspaceDir. Please check directory permissions."
120
+ }
121
+
122
+ $stamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ")
123
+ $createdAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
124
+ $id = Get-RandomId
125
+ $patchPath = Join-Path $workspaceDir "patch-$stamp-$id.patch"
126
+
127
+ try {
128
+ New-Item -ItemType File -Path $patchPath -Force | Out-Null
129
+ }
130
+ catch {
131
+ Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create a temporary patch file at $patchPath. Please check directory permissions and available disk space."
132
+ }
133
+
134
+ $result = [ordered]@{
135
+ ok = $true
136
+ id = $id
137
+ createdAt = $createdAt
138
+ path = $patchPath
139
+ ttlHours = $TTL_HOURS
140
+ }
141
+
142
+ $result | ConvertTo-Json -Compress | Write-Output
@@ -288,12 +288,20 @@ function Build-Reminder {
288
288
  # Project-local script; fallback to global
289
289
  $checkpointSh = "~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh"
290
290
  $checkpointPs1 = "`$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/checkpoint.ps1"
291
+ $allocPatchSh = "~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh"
292
+ $allocPatchPs1 = "`$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.ps1"
291
293
  if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/checkpoint.sh")) {
292
294
  $checkpointSh = ".vibe-x/agent-better-checkpoint/checkpoint.sh"
293
295
  }
294
296
  if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/checkpoint.ps1")) {
295
297
  $checkpointPs1 = ".\.vibe-x\agent-better-checkpoint\checkpoint.ps1"
296
298
  }
299
+ if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/alloc_patch.sh")) {
300
+ $allocPatchSh = ".vibe-x/agent-better-checkpoint/alloc_patch.sh"
301
+ }
302
+ if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/alloc_patch.ps1")) {
303
+ $allocPatchPs1 = ".\.vibe-x\agent-better-checkpoint\alloc_patch.ps1"
304
+ }
297
305
 
298
306
  return @"
299
307
  ## ⚠️ Uncommitted Changes Detected
@@ -305,17 +313,24 @@ There are uncommitted changes in the workspace. Please create a checkpoint commi
305
313
  $ChangesIndented
306
314
  ``````
307
315
 
308
- **Action Required**: Run the checkpoint script to commit these changes:
316
+ **Action Required**: Allocate a patch file, write only the selected hunks for this conversation, then run the checkpoint script.
309
317
 
310
318
  **macOS/Linux:**
311
319
  ``````bash
312
- $checkpointSh "checkpoint(<scope>): <description>" "<user-prompt>" --type fallback
320
+ PATCH_JSON=$($allocPatchSh --workspace "$Workspace")
321
+ PATCH_PATH=$(printf '%s' "$PATCH_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
322
+ # Write only the selected hunks for this conversation to "$PATCH_PATH" as a unified diff patch.
323
+ $checkpointSh "checkpoint(<scope>): <description>" "<user-prompt>" --type fallback --patch-file "$PATCH_PATH"
313
324
  ``````
314
325
 
315
326
  **Windows (PowerShell):**
316
327
  ``````powershell
317
- powershell -File "$checkpointPs1" "checkpoint(<scope>): <description>" "<user-prompt>" -Type fallback
328
+ $patch = powershell -File "$allocPatchPs1" -Workspace "$Workspace" | ConvertFrom-Json
329
+ # Write only the selected hunks for this conversation to $patch.path as a unified diff patch.
330
+ powershell -File "$checkpointPs1" "checkpoint(<scope>): <description>" "<user-prompt>" -Type fallback -PatchFile $patch.path
318
331
  ``````
332
+
333
+ If the checkpoint script returns an `ABC_PATCH_*` error, do not fall back to a full commit automatically. Explain the conflict and ask the user how to proceed.
319
334
  "@
320
335
  }
321
336
 
@@ -6,7 +6,7 @@
6
6
  AI provides descriptive content (subject + body). This script:
7
7
  1. Truncates user-prompt (≤60 chars, head+tail)
8
8
  2. Appends metadata via git interpret-trailers
9
- 3. Runs git add -A && git commit
9
+ 3. Runs git add -A && git commit, or applies a selected patch to the index first
10
10
 
11
11
  .PARAMETER Message
12
12
  Full commit message (subject + blank line + body). Required.
@@ -17,33 +17,77 @@
17
17
  .PARAMETER Type
18
18
  Checkpoint type: "auto" (default) or "fallback".
19
19
 
20
+ .PARAMETER PatchFile
21
+ Optional patch file path. When provided, only the selected hunks are staged before commit.
22
+
20
23
  .EXAMPLE
21
24
  .\checkpoint.ps1 "checkpoint(auth): add JWT refresh" "implement token refresh"
22
25
  #>
23
26
 
24
27
  param(
25
- [Parameter(Mandatory = $true, Position = 0)]
26
- [string]$Message,
28
+ [Parameter(Position = 0)]
29
+ [string]$Message = "",
27
30
 
28
31
  [Parameter(Position = 1)]
29
32
  [string]$UserPrompt = "",
30
33
 
31
- [string]$Type = "auto"
34
+ [string]$Type = "auto",
35
+
36
+ [string]$PatchFile = "",
37
+
38
+ [switch]$Help
32
39
  )
33
40
 
34
41
  $ErrorActionPreference = "Stop"
35
42
 
43
+ function Show-Usage {
44
+ @"
45
+ checkpoint.ps1 — Create semantic Git checkpoint commits
46
+
47
+ Usage:
48
+ checkpoint.ps1 <message> [user-prompt] [-Type auto|fallback] [-PatchFile <path>]
49
+ checkpoint.ps1 -Help
50
+
51
+ Options:
52
+ -Type <auto|fallback> Checkpoint type metadata
53
+ -PatchFile <path> Apply only the selected patch hunks to the index before commit
54
+ -Help Show this help message
55
+ "@ | Write-Output
56
+ }
57
+
58
+ function Fail-Checkpoint {
59
+ param(
60
+ [string]$Code,
61
+ [string]$MessageText
62
+ )
63
+
64
+ [Console]::Error.WriteLine("${Code}: ${MessageText}")
65
+ exit 1
66
+ }
67
+
68
+ if ($Help) {
69
+ Show-Usage
70
+ exit 0
71
+ }
72
+
73
+ if (-not $Message) {
74
+ [Console]::Error.WriteLine("Error: commit message is required")
75
+ [Console]::Error.WriteLine("Usage: checkpoint.ps1 <message> [user-prompt] [-Type auto|fallback] [-PatchFile <path>]")
76
+ exit 1
77
+ }
78
+
36
79
  # ============================================================
37
80
  # Platform detection
38
81
  # ============================================================
39
82
 
40
83
  function Detect-Platform {
41
- if ($env:CLAUDE_CODE -or (Get-Command claude -ErrorAction SilentlyContinue)) {
42
- return "claude-code"
43
- }
44
- if ($env:CURSOR_VERSION -or $env:CURSOR_TRACE_ID) {
84
+ # 优先检测运行时环境变量(谁在调用),而非安装态(Get-Command
85
+ if ($env:CURSOR_AGENT -or $env:CURSOR_TRACE_ID -or $env:CURSOR_VERSION) {
45
86
  return "cursor"
46
87
  }
88
+ if ($env:CLAUDE_CODE) {
89
+ return "claude-code"
90
+ }
47
91
  return "unknown"
48
92
  }
49
93
 
@@ -79,11 +123,11 @@ if ($UserPrompt) {
79
123
 
80
124
  function Test-HasChanges {
81
125
  # Staged changes
82
- $diffCached = git diff --cached --quiet 2>$null
126
+ git diff --cached --quiet 2>$null
83
127
  if ($LASTEXITCODE -ne 0) { return $true }
84
128
 
85
129
  # Unstaged changes
86
- $diffWorking = git diff --quiet 2>$null
130
+ git diff --quiet 2>$null
87
131
  if ($LASTEXITCODE -ne 0) { return $true }
88
132
 
89
133
  # Untracked files
@@ -93,16 +137,46 @@ function Test-HasChanges {
93
137
  return $false
94
138
  }
95
139
 
96
- if (-not (Test-HasChanges)) {
97
- Write-Host "No changes to commit."
98
- exit 0
140
+ function Test-HasStagedChanges {
141
+ git diff --cached --quiet 2>$null
142
+ return $LASTEXITCODE -ne 0
99
143
  }
100
144
 
101
- # ============================================================
102
- # git add -A
103
- # ============================================================
145
+ if (-not $PatchFile) {
146
+ if (-not (Test-HasChanges)) {
147
+ Write-Host "No changes to commit."
148
+ exit 0
149
+ }
150
+
151
+ # ============================================================
152
+ # git add -A
153
+ # ============================================================
154
+ git add -A
155
+ }
156
+ else {
157
+ if (-not (Test-Path $PatchFile -PathType Leaf)) {
158
+ Fail-Checkpoint -Code "ABC_PATCH_FILE_MISSING" -MessageText "Patch file not found at $PatchFile. Please allocate and write the patch file before running checkpoint."
159
+ }
104
160
 
105
- git add -A
161
+ $patchInfo = Get-Item $PatchFile
162
+ if ($patchInfo.Length -le 0) {
163
+ Fail-Checkpoint -Code "ABC_PATCH_FILE_EMPTY" -MessageText "Patch file at $PatchFile is empty. Please write at least one selected hunk before running checkpoint."
164
+ }
165
+
166
+ git apply --cached --check -- $PatchFile 2>$null
167
+ if ($LASTEXITCODE -ne 0) {
168
+ Fail-Checkpoint -Code "ABC_PATCH_APPLY_FAILED" -MessageText "Failed to apply the selected patch cleanly to the Git index. The target hunks may have drifted or overlap with other uncommitted edits."
169
+ }
170
+
171
+ git apply --cached -- $PatchFile 2>$null
172
+ if ($LASTEXITCODE -ne 0) {
173
+ Fail-Checkpoint -Code "ABC_PATCH_APPLY_FAILED" -MessageText "Failed to apply the selected patch to the Git index after the dry run succeeded. Please verify the patch file and retry."
174
+ }
175
+
176
+ if (-not (Test-HasStagedChanges)) {
177
+ Fail-Checkpoint -Code "ABC_PATCH_NO_STAGED_CHANGES" -MessageText "The selected patch did not produce any staged changes. The target hunks may already be staged or no longer match the current repository state."
178
+ }
179
+ }
106
180
 
107
181
  # ============================================================
108
182
  # Build trailers and commit
@@ -117,7 +191,16 @@ if ($TruncatedPrompt) {
117
191
  $TrailerArgs += @("--trailer", "User-Prompt: $TruncatedPrompt")
118
192
  }
119
193
 
120
- # Pipe message → git interpret-trailers → git commit
121
194
  $Message | git interpret-trailers @TrailerArgs | git commit -F -
195
+ if ($LASTEXITCODE -ne 0) {
196
+ if ($PatchFile) {
197
+ Fail-Checkpoint -Code "ABC_PATCH_COMMIT_FAILED" -MessageText "Git commit failed after staging the selected patch. Review the repository state and retry when ready."
198
+ }
199
+ exit $LASTEXITCODE
200
+ }
201
+
202
+ if ($PatchFile -and (Test-Path $PatchFile -PathType Leaf)) {
203
+ Remove-Item $PatchFile -Force -ErrorAction SilentlyContinue
204
+ }
122
205
 
123
206
  Write-Host "Checkpoint committed successfully."
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Release Gate for agent-better-checkpoint.
4
+ * Fails fast when common release omissions are detected.
5
+ */
6
+
7
+ import { readFileSync } from 'node:fs';
8
+ import { createHash } from 'node:crypto';
9
+ import { execFileSync } from 'node:child_process';
10
+
11
+ function fail(msg) {
12
+ console.error(`\n[release:check] ${msg}`);
13
+ process.exit(1);
14
+ }
15
+
16
+ function ok(msg) {
17
+ console.log(`[release:check] OK: ${msg}`);
18
+ }
19
+
20
+ function fileExists(path) {
21
+ try {
22
+ readFileSync(path);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function sha256(text) {
30
+ return createHash('sha256').update(text, 'utf8').digest('hex');
31
+ }
32
+
33
+ function readText(path) {
34
+ return readFileSync(path, 'utf8');
35
+ }
36
+
37
+ function git(args) {
38
+ return execFileSync('git', args, { encoding: 'utf8' }).trim();
39
+ }
40
+
41
+ function ensureCleanWorktree() {
42
+ const out = git(['status', '--porcelain']);
43
+ if (out.length !== 0) fail('Working tree is not clean. Commit or stash changes before release.');
44
+ ok('working tree clean');
45
+ }
46
+
47
+ function ensureChangelogHasVersion(version) {
48
+ const changelog = readText('CHANGELOG.md');
49
+ if (!changelog.includes(`## [${version}]`)) {
50
+ fail(`CHANGELOG.md does not contain an entry for version ${version}.`);
51
+ }
52
+ ok(`CHANGELOG contains ${version}`);
53
+ }
54
+
55
+ function ensureFilesInSync(sourcePath, mirrorPath, label) {
56
+ if (!fileExists(mirrorPath)) {
57
+ console.warn(`[release:check] WARN: ${mirrorPath} not found; skipping sync check for ${label}.`);
58
+ return;
59
+ }
60
+
61
+ const sourceText = readText(sourcePath).replace(/\r\n/g, '\n');
62
+ const mirrorText = readText(mirrorPath).replace(/\r\n/g, '\n');
63
+
64
+ if (sha256(sourceText) !== sha256(mirrorText)) {
65
+ fail(`${label} out of sync:\n- ${sourcePath}\n- ${mirrorPath}\nPlease update both or regenerate project-local copy.`);
66
+ }
67
+
68
+ ok(`${label} copies are in sync`);
69
+ }
70
+
71
+ function ensureProjectLocalScriptsInSync() {
72
+ const pairs = [
73
+ {
74
+ sourcePath: 'platform/unix/checkpoint.sh',
75
+ mirrorPath: '.vibe-x/agent-better-checkpoint/checkpoint.sh',
76
+ label: 'unix checkpoint.sh',
77
+ },
78
+ {
79
+ sourcePath: 'platform/unix/check_uncommitted.sh',
80
+ mirrorPath: '.vibe-x/agent-better-checkpoint/check_uncommitted.sh',
81
+ label: 'unix check_uncommitted.sh',
82
+ },
83
+ {
84
+ sourcePath: 'platform/win/checkpoint.ps1',
85
+ mirrorPath: '.vibe-x/agent-better-checkpoint/checkpoint.ps1',
86
+ label: 'windows checkpoint.ps1',
87
+ },
88
+ {
89
+ sourcePath: 'platform/win/check_uncommitted.ps1',
90
+ mirrorPath: '.vibe-x/agent-better-checkpoint/check_uncommitted.ps1',
91
+ label: 'windows check_uncommitted.ps1',
92
+ },
93
+ {
94
+ sourcePath: 'platform/unix/alloc_patch.sh',
95
+ mirrorPath: '.vibe-x/agent-better-checkpoint/alloc_patch.sh',
96
+ label: 'unix alloc_patch.sh',
97
+ },
98
+ {
99
+ sourcePath: 'platform/win/alloc_patch.ps1',
100
+ mirrorPath: '.vibe-x/agent-better-checkpoint/alloc_patch.ps1',
101
+ label: 'windows alloc_patch.ps1',
102
+ },
103
+ ];
104
+
105
+ for (const pair of pairs) {
106
+ ensureFilesInSync(pair.sourcePath, pair.mirrorPath, pair.label);
107
+ }
108
+ }
109
+
110
+ function ensureSkillVersionMatches(version) {
111
+ const skill = readText('skill/SKILL.md');
112
+ const requiredSnippets = [
113
+ `version: "${version}"`,
114
+ `@vibe-x/agent-better-checkpoint@${version}`,
115
+ `**Version**: ${version}`,
116
+ ];
117
+
118
+ const missing = requiredSnippets.filter((snippet) => !skill.includes(snippet));
119
+ if (missing.length > 0) {
120
+ fail(`skill/SKILL.md is missing current version references for ${version}:\n- ${missing.join('\n- ')}`);
121
+ }
122
+
123
+ ok(`skill/SKILL.md references ${version}`);
124
+ }
125
+
126
+ function main() {
127
+ if (!fileExists('package.json')) fail('package.json not found (run from repo root).');
128
+ if (!fileExists('CHANGELOG.md')) fail('CHANGELOG.md not found.');
129
+ if (!fileExists('platform/unix/checkpoint.sh')) fail('platform/unix/checkpoint.sh not found.');
130
+
131
+ const pkg = JSON.parse(readText('package.json'));
132
+ const version = pkg.version;
133
+ if (!version) fail('package.json missing version field.');
134
+
135
+ ensureCleanWorktree();
136
+ ensureProjectLocalScriptsInSync();
137
+ ensureChangelogHasVersion(version);
138
+ ensureSkillVersionMatches(version);
139
+
140
+ ok(`release checks passed for ${pkg.name}@${version}`);
141
+ }
142
+
143
+ main();
package/skill/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: agent-better-checkpoint
3
- description: "Automatically creates semantic Git checkpoint commits during AI coding sessions. Replaces opaque platform checkpoints with transparent, queryable Git commits using Conventional Commits format with Git Trailers. You MUST follow this skill whenever you make code changes commit after each meaningful edit."
3
+ description: "Creates semantic Git checkpoint commits during AI coding sessions with Conventional Commits format and Git Trailers. Use after completing code edits, or when user says 'commit changes', 'create checkpoint', or 'save my progress'."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.3.2"
6
+ version: "0.3.4"
7
7
  author: "alienzhou"
8
8
  category: "version-control"
9
9
  ---
@@ -31,7 +31,7 @@ Both `.sh` and `.ps1` are always installed regardless of current OS.
31
31
  If neither exists, run:
32
32
 
33
33
  ```bash
34
- npx @vibe-x/agent-better-checkpoint@0.3.2
34
+ npx @vibe-x/agent-better-checkpoint@0.3.4
35
35
  ```
36
36
 
37
37
  Without `--target`: installs globally. With `--target .`: project-only (skill + hooks in `.cursor/`, scripts in `.vibe-x/`), no global changes.
@@ -99,14 +99,16 @@ Switch to flex-column layout with collapsible sidebar.
99
99
 
100
100
  ## 🛠️ How to Commit
101
101
 
102
- Call the checkpoint script after composing your message. Both `.sh` and `.ps1` are always available pick the one matching the current OS.
102
+ Default to the patch-based flow when you can clearly identify the hunks from the current logical unit of work. Fall back to the full-workspace commit flow only when the user explicitly wants everything committed together.
103
103
 
104
104
  **Prefer project-local when present**, fall back to global:
105
105
 
106
- | OS | Project-local | Global fallback |
107
- |----|--------------|-----------------|
108
- | macOS/Linux | `.vibe-x/agent-better-checkpoint/checkpoint.sh` | `~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh` |
109
- | Windows | `powershell -File ".vibe-x\agent-better-checkpoint\checkpoint.ps1"` | `powershell -File "$env:USERPROFILE\.vibe-x\agent-better-checkpoint\scripts\checkpoint.ps1"` |
106
+ | Script | macOS/Linux | Windows |
107
+ |--------|-------------|---------|
108
+ | checkpoint | `.vibe-x/agent-better-checkpoint/checkpoint.sh` | `powershell -File ".vibe-x\agent-better-checkpoint\checkpoint.ps1"` |
109
+ | alloc_patch | `.vibe-x/agent-better-checkpoint/alloc_patch.sh` | `powershell -File ".vibe-x\agent-better-checkpoint\alloc_patch.ps1"` |
110
+ | global checkpoint | `~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh` | `powershell -File "$env:USERPROFILE\.vibe-x\agent-better-checkpoint\scripts\checkpoint.ps1"` |
111
+ | global alloc_patch | `~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh` | `powershell -File "$env:USERPROFILE\.vibe-x\agent-better-checkpoint\scripts\alloc_patch.ps1"` |
110
112
 
111
113
  ### Parameters:
112
114
 
@@ -115,31 +117,51 @@ Call the checkpoint script after composing your message. Both `.sh` and `.ps1` a
115
117
  | message (1st arg) | Yes | Full commit message (subject + blank line + body) |
116
118
  | user-prompt (2nd arg) | No | The user's original prompt/request |
117
119
  | `--type` / `-Type` | No | `auto` (default) or `fallback` |
120
+ | `--patch-file` / `-PatchFile` | No | Apply only the selected hunks from a unified diff patch before commit |
118
121
 
119
- ### Example (macOS/Linux):
122
+ ### Recommended patch flow (macOS/Linux):
120
123
 
121
124
  ```bash
125
+ PATCH_JSON=$(.vibe-x/agent-better-checkpoint/alloc_patch.sh --workspace "$PWD")
126
+ PATCH_PATH=$(printf '%s' "$PATCH_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
127
+ # Write only the selected hunks for this logical unit to "$PATCH_PATH" as a unified diff patch.
122
128
  .vibe-x/agent-better-checkpoint/checkpoint.sh \
123
129
  "checkpoint(auth): add JWT token refresh logic
124
130
 
125
131
  Implement automatic token refresh when access token expires.
126
132
  Uses refresh token rotation for security." \
127
- "帮我实现 token 刷新机制"
133
+ "帮我实现 token 刷新机制" \
134
+ --patch-file "$PATCH_PATH"
128
135
  ```
129
136
 
130
- ### Example (Windows):
137
+ ### Recommended patch flow (Windows):
131
138
 
132
139
  ```powershell
140
+ $patch = powershell -File ".vibe-x\agent-better-checkpoint\alloc_patch.ps1" -Workspace (Get-Location).Path | ConvertFrom-Json
141
+ # Write only the selected hunks for this logical unit to $patch.path as a unified diff patch.
133
142
  powershell -File ".vibe-x\agent-better-checkpoint\checkpoint.ps1" `
134
143
  "checkpoint(auth): add JWT token refresh logic`n`nImplement automatic token refresh when access token expires.`nUses refresh token rotation for security." `
144
+ "帮我实现 token 刷新机制" `
145
+ -PatchFile $patch.path
146
+ ```
147
+
148
+ ### Full-workspace fallback example (macOS/Linux):
149
+
150
+ ```bash
151
+ .vibe-x/agent-better-checkpoint/checkpoint.sh \
152
+ "checkpoint(auth): add JWT token refresh logic
153
+
154
+ Implement automatic token refresh when access token expires.
155
+ Uses refresh token rotation for security." \
135
156
  "帮我实现 token 刷新机制"
136
157
  ```
137
158
 
138
159
  ### What the script does:
139
160
  1. Truncates user prompt to ≤60 characters (head...tail)
140
161
  2. Appends Git Trailers: `Agent`, `Checkpoint-Type`, `User-Prompt`
141
- 3. Runs `git add -A && git commit`
142
- 4. Exits gracefully if there are no changes
162
+ 3. Either runs `git add -A && git commit` or applies the selected patch hunks and then commits
163
+ 4. Returns `ABC_PATCH_*` errors when the selected patch cannot be committed safely
164
+ 5. Exits gracefully if there are no changes
143
165
 
144
166
  ---
145
167
 
@@ -167,4 +189,4 @@ This should feel natural — commit as you go, like any good developer.
167
189
 
168
190
  ---
169
191
 
170
- **Version**: 0.3.2
192
+ **Version**: 0.3.4