@vibe-x/agent-better-checkpoint 0.3.3 → 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
  `);
@@ -229,11 +233,14 @@ function installScripts(osType) {
229
233
 
230
234
  // 双端脚本都安装,方便跨平台使用
231
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'));
232
237
  copyFileSafe(join(PLATFORM_DIR, 'unix', 'check_uncommitted.sh'), join(hooksDir, 'check_uncommitted.sh'));
233
238
  setExecutable(join(scriptsDir, 'checkpoint.sh'));
239
+ setExecutable(join(scriptsDir, 'alloc_patch.sh'));
234
240
  setExecutable(join(hooksDir, 'check_uncommitted.sh'));
235
241
 
236
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'));
237
244
  copyFileSafe(join(PLATFORM_DIR, 'win', 'check_uncommitted.ps1'), join(hooksDir, 'check_uncommitted.ps1'));
238
245
 
239
246
  console.log(` Scripts → ${scriptsDir}/`);
@@ -301,12 +308,18 @@ function registerCursorHook(osType) {
301
308
  function installProjectOnly(targetDir, aiPlatform, osType) {
302
309
  const root = resolve(targetDir);
303
310
 
304
- // .vibe-x/agent-better-checkpoint: checkpoint 脚本 + config
311
+ // .vibe-x/agent-better-checkpoint: checkpoint 脚本 + helper + config
305
312
  const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
306
313
  ensureDir(vibeXBase);
307
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'));
308
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'));
309
320
  setExecutable(join(vibeXBase, 'checkpoint.sh'));
321
+ setExecutable(join(vibeXBase, 'alloc_patch.sh'));
322
+ setExecutable(join(vibeXBase, 'check_uncommitted.sh'));
310
323
  const configDest = join(vibeXBase, 'config.yml');
311
324
  if (!existsSync(configDest) && existsSync(CONFIG_TEMPLATE)) {
312
325
  copyFileSafe(CONFIG_TEMPLATE, configDest);
@@ -356,9 +369,22 @@ function uninstallProjectOnly(targetDir, aiPlatform) {
356
369
  const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
357
370
  const skillRoot = aiPlatform === 'cursor' ? '.cursor' : '.claude';
358
371
  const skillDir = join(root, skillRoot, 'skills', SKILL_NAME);
359
- if (existsSync(vibeXBase)) {
360
- rmSync(vibeXBase, { recursive: true, force: true });
361
- 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 });
362
388
  }
363
389
 
364
390
  if (existsSync(skillDir)) {
@@ -503,6 +529,123 @@ function unregisterClaudeHook() {
503
529
  console.log(` Cleaned config: ${settingsPath}`);
504
530
  }
505
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
+
506
649
  // ============================================================
507
650
  // Main entry
508
651
  // ============================================================
@@ -513,7 +656,7 @@ function main() {
513
656
  const aiPlatform = args.platform || detectAIPlatform();
514
657
  const projectTargetDir = args.target ? resolve(args.target) : null;
515
658
 
516
- if (!aiPlatform && !projectTargetDir && !args.uninstall) {
659
+ if (!aiPlatform && !projectTargetDir && !args.uninstall && !args.activate) {
517
660
  console.error(
518
661
  'Error: could not detect AI platform.\n' +
519
662
  'Please specify: npx @vibe-x/agent-better-checkpoint --platform cursor|claude'
@@ -556,6 +699,10 @@ function main() {
556
699
  if (platforms.length === 0) console.log('\nNo global installation found.');
557
700
  }
558
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);
559
706
  } else {
560
707
  if (projectTargetDir) {
561
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.3",
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,18 +27,44 @@ 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
  # ============================================================
@@ -60,10 +87,23 @@ AGENT_PLATFORM=$(detect_platform)
60
87
  truncate_prompt() {
61
88
  local prompt="$1"
62
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
+
63
97
  local len=${#prompt}
64
98
 
65
99
  if [[ $len -le $max_len ]]; then
66
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
67
107
  return
68
108
  fi
69
109
 
@@ -71,6 +111,14 @@ truncate_prompt() {
71
111
  local tail_len=$(( max_len - 3 - head_len ))
72
112
  local head="${prompt:0:$head_len}"
73
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
+
74
122
  echo "${head}...${tail}"
75
123
  }
76
124
 
@@ -98,15 +146,44 @@ has_changes() {
98
146
  return 1
99
147
  }
100
148
 
101
- if ! has_changes; then
102
- echo "No changes to commit."
103
- exit 0
104
- fi
149
+ has_staged_changes() {
150
+ if ! git diff --cached --quiet 2>/dev/null; then
151
+ return 0
152
+ fi
153
+ return 1
154
+ }
105
155
 
106
- # ============================================================
107
- # git add -A
108
- # ============================================================
109
- 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
110
187
 
111
188
  # ============================================================
112
189
  # Build trailers and commit
@@ -120,6 +197,15 @@ if [[ -n "$TRUNCATED_PROMPT" ]]; then
120
197
  TRAILER_ARGS+=(--trailer "User-Prompt: ${TRUNCATED_PROMPT}")
121
198
  fi
122
199
 
123
- 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
124
210
 
125
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,22 +17,65 @@
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
  # ============================================================
@@ -80,11 +123,11 @@ if ($UserPrompt) {
80
123
 
81
124
  function Test-HasChanges {
82
125
  # Staged changes
83
- $diffCached = git diff --cached --quiet 2>$null
126
+ git diff --cached --quiet 2>$null
84
127
  if ($LASTEXITCODE -ne 0) { return $true }
85
128
 
86
129
  # Unstaged changes
87
- $diffWorking = git diff --quiet 2>$null
130
+ git diff --quiet 2>$null
88
131
  if ($LASTEXITCODE -ne 0) { return $true }
89
132
 
90
133
  # Untracked files
@@ -94,16 +137,46 @@ function Test-HasChanges {
94
137
  return $false
95
138
  }
96
139
 
97
- if (-not (Test-HasChanges)) {
98
- Write-Host "No changes to commit."
99
- exit 0
140
+ function Test-HasStagedChanges {
141
+ git diff --cached --quiet 2>$null
142
+ return $LASTEXITCODE -ne 0
100
143
  }
101
144
 
102
- # ============================================================
103
- # git add -A
104
- # ============================================================
145
+ if (-not $PatchFile) {
146
+ if (-not (Test-HasChanges)) {
147
+ Write-Host "No changes to commit."
148
+ exit 0
149
+ }
105
150
 
106
- git add -A
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
+ }
160
+
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
+ }
107
180
 
108
181
  # ============================================================
109
182
  # Build trailers and commit
@@ -118,7 +191,16 @@ if ($TruncatedPrompt) {
118
191
  $TrailerArgs += @("--trailer", "User-Prompt: $TruncatedPrompt")
119
192
  }
120
193
 
121
- # Pipe message → git interpret-trailers → git commit
122
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
+ }
123
205
 
124
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
@@ -3,7 +3,7 @@ name: agent-better-checkpoint
3
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.3"
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.3
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.3
192
+ **Version**: 0.3.4