aether-colony 3.1.4 → 3.1.15

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.
Files changed (124) hide show
  1. package/.claude/commands/ant/archaeology.md +12 -0
  2. package/.claude/commands/ant/build.md +382 -319
  3. package/.claude/commands/ant/chaos.md +23 -1
  4. package/.claude/commands/ant/colonize.md +147 -87
  5. package/.claude/commands/ant/continue.md +213 -23
  6. package/.claude/commands/ant/council.md +22 -0
  7. package/.claude/commands/ant/dream.md +18 -0
  8. package/.claude/commands/ant/entomb.md +178 -6
  9. package/.claude/commands/ant/init.md +87 -13
  10. package/.claude/commands/ant/lay-eggs.md +45 -5
  11. package/.claude/commands/ant/oracle.md +82 -9
  12. package/.claude/commands/ant/organize.md +2 -2
  13. package/.claude/commands/ant/pause-colony.md +86 -28
  14. package/.claude/commands/ant/phase.md +26 -0
  15. package/.claude/commands/ant/plan.md +204 -111
  16. package/.claude/commands/ant/resume-colony.md +23 -1
  17. package/.claude/commands/ant/resume.md +159 -0
  18. package/.claude/commands/ant/seal.md +177 -3
  19. package/.claude/commands/ant/swarm.md +78 -97
  20. package/.claude/commands/ant/verify-castes.md +7 -7
  21. package/.claude/commands/ant/watch.md +17 -0
  22. package/.opencode/agents/aether-ambassador.md +97 -0
  23. package/.opencode/agents/aether-archaeologist.md +91 -0
  24. package/.opencode/agents/aether-architect.md +66 -0
  25. package/.opencode/agents/aether-auditor.md +111 -0
  26. package/.opencode/agents/aether-builder.md +28 -10
  27. package/.opencode/agents/aether-chaos.md +98 -0
  28. package/.opencode/agents/aether-chronicler.md +80 -0
  29. package/.opencode/agents/aether-gatekeeper.md +107 -0
  30. package/.opencode/agents/aether-guardian.md +107 -0
  31. package/.opencode/agents/aether-includer.md +108 -0
  32. package/.opencode/agents/aether-keeper.md +106 -0
  33. package/.opencode/agents/aether-measurer.md +119 -0
  34. package/.opencode/agents/aether-probe.md +91 -0
  35. package/.opencode/agents/aether-queen.md +72 -19
  36. package/.opencode/agents/aether-route-setter.md +85 -0
  37. package/.opencode/agents/aether-sage.md +98 -0
  38. package/.opencode/agents/aether-scout.md +33 -15
  39. package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
  40. package/.opencode/agents/aether-surveyor-nest.md +272 -0
  41. package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
  42. package/.opencode/agents/aether-surveyor-provisions.md +277 -0
  43. package/.opencode/agents/aether-tracker.md +91 -0
  44. package/.opencode/agents/aether-watcher.md +30 -12
  45. package/.opencode/agents/aether-weaver.md +87 -0
  46. package/.opencode/agents/workers.md +1034 -0
  47. package/.opencode/commands/ant/archaeology.md +44 -26
  48. package/.opencode/commands/ant/build.md +327 -295
  49. package/.opencode/commands/ant/chaos.md +32 -4
  50. package/.opencode/commands/ant/colonize.md +119 -93
  51. package/.opencode/commands/ant/continue.md +98 -10
  52. package/.opencode/commands/ant/council.md +28 -0
  53. package/.opencode/commands/ant/dream.md +24 -0
  54. package/.opencode/commands/ant/entomb.md +73 -1
  55. package/.opencode/commands/ant/feedback.md +8 -2
  56. package/.opencode/commands/ant/flag.md +9 -3
  57. package/.opencode/commands/ant/flags.md +8 -2
  58. package/.opencode/commands/ant/focus.md +8 -2
  59. package/.opencode/commands/ant/help.md +12 -0
  60. package/.opencode/commands/ant/init.md +49 -4
  61. package/.opencode/commands/ant/lay-eggs.md +30 -2
  62. package/.opencode/commands/ant/oracle.md +39 -7
  63. package/.opencode/commands/ant/organize.md +9 -3
  64. package/.opencode/commands/ant/pause-colony.md +54 -1
  65. package/.opencode/commands/ant/phase.md +36 -4
  66. package/.opencode/commands/ant/plan.md +225 -117
  67. package/.opencode/commands/ant/redirect.md +8 -2
  68. package/.opencode/commands/ant/resume-colony.md +51 -26
  69. package/.opencode/commands/ant/seal.md +76 -0
  70. package/.opencode/commands/ant/status.md +50 -20
  71. package/.opencode/commands/ant/swarm.md +108 -104
  72. package/.opencode/commands/ant/tunnels.md +107 -2
  73. package/CHANGELOG.md +21 -0
  74. package/README.md +199 -86
  75. package/bin/cli.js +142 -25
  76. package/bin/generate-commands.sh +100 -16
  77. package/bin/lib/caste-colors.js +5 -5
  78. package/bin/lib/errors.js +16 -0
  79. package/bin/lib/file-lock.js +279 -44
  80. package/bin/lib/state-sync.js +206 -23
  81. package/bin/lib/update-transaction.js +206 -24
  82. package/bin/sync-to-runtime.sh +129 -0
  83. package/package.json +2 -2
  84. package/runtime/CONTEXT.md +160 -0
  85. package/runtime/aether-utils.sh +1421 -55
  86. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
  87. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
  88. package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
  89. package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
  90. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
  91. package/runtime/docs/README.md +94 -0
  92. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
  93. package/runtime/docs/biological-reference.md +272 -0
  94. package/runtime/docs/codebase-review.md +399 -0
  95. package/runtime/docs/command-sync.md +164 -0
  96. package/runtime/docs/implementation-learnings.md +89 -0
  97. package/runtime/docs/known-issues.md +217 -0
  98. package/runtime/docs/namespace.md +148 -0
  99. package/runtime/docs/planning-discipline.md +159 -0
  100. package/runtime/lib/queen-utils.sh +729 -0
  101. package/runtime/model-profiles.yaml +100 -0
  102. package/runtime/recover.sh +136 -0
  103. package/runtime/templates/QUEEN.md.template +79 -0
  104. package/runtime/utils/atomic-write.sh +5 -5
  105. package/runtime/utils/chamber-utils.sh +6 -3
  106. package/runtime/utils/error-handler.sh +200 -0
  107. package/runtime/utils/queen-to-md.xsl +395 -0
  108. package/runtime/utils/spawn-tree.sh +428 -0
  109. package/runtime/utils/spawn-with-model.sh +56 -0
  110. package/runtime/utils/state-loader.sh +215 -0
  111. package/runtime/utils/swarm-display.sh +5 -5
  112. package/runtime/utils/watch-spawn-tree.sh +90 -22
  113. package/runtime/utils/xml-compose.sh +247 -0
  114. package/runtime/utils/xml-core.sh +186 -0
  115. package/runtime/utils/xml-utils.sh +2161 -0
  116. package/runtime/verification-loop.md +1 -1
  117. package/runtime/workers-new-castes.md +516 -0
  118. package/runtime/workers.md +20 -8
  119. package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
  120. package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
  121. package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
  122. package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
  123. package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
  124. package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
package/bin/cli.js CHANGED
@@ -70,11 +70,9 @@ const COMMANDS_DEST = path.join(HOME, '.claude', 'commands', 'ant');
70
70
 
71
71
  // Hub paths
72
72
  const HUB_DIR = path.join(HOME, '.aether');
73
- const HUB_SYSTEM = path.join(HUB_DIR, 'system');
74
73
  const HUB_COMMANDS_CLAUDE = path.join(HUB_DIR, 'commands', 'claude');
75
74
  const HUB_COMMANDS_OPENCODE = path.join(HUB_DIR, 'commands', 'opencode');
76
75
  const HUB_AGENTS = path.join(HUB_DIR, 'agents');
77
- const HUB_VISUALIZATIONS = path.join(HUB_DIR, 'visualizations');
78
76
  const HUB_REGISTRY = path.join(HUB_DIR, 'registry.json');
79
77
  const HUB_VERSION = path.join(HUB_DIR, 'version.json');
80
78
 
@@ -836,6 +834,122 @@ function gitStashFiles(repoPath, files) {
836
834
  }
837
835
  }
838
836
 
837
+ // Directories to exclude from hub sync (user data, local state)
838
+ const HUB_EXCLUDE_DIRS = ['data', 'dreams', 'checkpoints', 'locks', 'temp'];
839
+
840
+ /**
841
+ * Check if a path should be excluded from hub sync
842
+ * @param {string} relPath - Relative path from .aether/
843
+ * @returns {boolean} True if should be excluded
844
+ */
845
+ function shouldExcludeFromHub(relPath) {
846
+ const parts = relPath.split(path.sep);
847
+ // Exclude if any part of the path is in the exclude list
848
+ return parts.some(part => HUB_EXCLUDE_DIRS.includes(part));
849
+ }
850
+
851
+ /**
852
+ * Sync .aether/ directory to hub, excluding user data directories
853
+ * @param {string} srcDir - Source .aether/ directory
854
+ * @param {string} destDir - Destination hub directory
855
+ * @returns {object} Sync result with copied, removed, skipped counts
856
+ */
857
+ function syncAetherToHub(srcDir, destDir) {
858
+ if (!fs.existsSync(srcDir)) {
859
+ return { copied: 0, removed: 0, skipped: 0 };
860
+ }
861
+
862
+ fs.mkdirSync(destDir, { recursive: true });
863
+
864
+ // Get all files in source, filtering out excluded directories
865
+ const srcFiles = [];
866
+ function collectFiles(dir, base) {
867
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
868
+ for (const entry of entries) {
869
+ if (entry.name.startsWith('.')) continue;
870
+ const fullPath = path.join(dir, entry.name);
871
+ const relPath = path.relative(base, fullPath);
872
+
873
+ if (shouldExcludeFromHub(relPath)) continue;
874
+
875
+ if (entry.isDirectory()) {
876
+ collectFiles(fullPath, base);
877
+ } else {
878
+ srcFiles.push(relPath);
879
+ }
880
+ }
881
+ }
882
+ collectFiles(srcDir, srcDir);
883
+
884
+ // Copy files with hash comparison
885
+ let copied = 0;
886
+ let skipped = 0;
887
+ for (const relPath of srcFiles) {
888
+ const srcPath = path.join(srcDir, relPath);
889
+ const destPath = path.join(destDir, relPath);
890
+
891
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
892
+
893
+ // Hash comparison
894
+ let shouldCopy = true;
895
+ if (fs.existsSync(destPath)) {
896
+ const srcHash = hashFileSync(srcPath);
897
+ const destHash = hashFileSync(destPath);
898
+ if (srcHash === destHash) {
899
+ shouldCopy = false;
900
+ skipped++;
901
+ }
902
+ }
903
+
904
+ if (shouldCopy) {
905
+ fs.copyFileSync(srcPath, destPath);
906
+ if (relPath.endsWith('.sh')) {
907
+ fs.chmodSync(destPath, 0o755);
908
+ }
909
+ copied++;
910
+ }
911
+ }
912
+
913
+ // Cleanup: remove files in dest that aren't in source (and aren't excluded)
914
+ const destFiles = [];
915
+ function collectDestFiles(dir, base) {
916
+ if (!fs.existsSync(dir)) return;
917
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
918
+ for (const entry of entries) {
919
+ if (entry.name.startsWith('.') || entry.name === 'registry.json' || entry.name === 'version.json' || entry.name === 'manifest.json') continue;
920
+ const fullPath = path.join(dir, entry.name);
921
+ const relPath = path.relative(base, fullPath);
922
+
923
+ if (shouldExcludeFromHub(relPath)) continue;
924
+
925
+ if (entry.isDirectory()) {
926
+ collectDestFiles(fullPath, base);
927
+ } else {
928
+ destFiles.push(relPath);
929
+ }
930
+ }
931
+ }
932
+ collectDestFiles(destDir, destDir);
933
+
934
+ const srcSet = new Set(srcFiles);
935
+ const removed = [];
936
+ for (const relPath of destFiles) {
937
+ if (!srcSet.has(relPath)) {
938
+ removed.push(relPath);
939
+ try {
940
+ fs.unlinkSync(path.join(destDir, relPath));
941
+ } catch (err) {
942
+ // Ignore cleanup errors
943
+ }
944
+ }
945
+ }
946
+
947
+ // Clean up empty directories
948
+ cleanEmptyDirs(destDir);
949
+
950
+ return { copied, removed, skipped };
951
+ }
952
+
839
953
  function setupHub() {
840
954
  // Create ~/.aether/ directory structure and populate from package
841
955
  try {
@@ -848,17 +962,35 @@ function setupHub() {
848
962
  log(` Warning: previous manifest is invalid, regenerating`);
849
963
  }
850
964
 
851
- // Sync runtime/ -> ~/.aether/system/
965
+ // Sync runtime/ -> ~/.aether/ (clean production files)
966
+ // runtime/ is the staging area - explicit allowlist via sync-to-runtime.sh
852
967
  const runtimeSrc = path.join(PACKAGE_DIR, 'runtime');
853
968
  if (fs.existsSync(runtimeSrc)) {
854
- const result = syncDirWithCleanup(runtimeSrc, HUB_SYSTEM);
855
- log(` Hub system: ${result.copied} files -> ${HUB_SYSTEM}`);
969
+ const result = syncAetherToHub(runtimeSrc, HUB_DIR);
970
+ log(` Hub system: ${result.copied} files, ${result.skipped} unchanged -> ${HUB_DIR}`);
856
971
  if (result.removed.length > 0) {
857
972
  log(` Hub system: removed ${result.removed.length} stale files`);
858
973
  for (const f of result.removed) log(` - ${f}`);
859
974
  }
860
975
  }
861
976
 
977
+ // Clean up legacy directories from old hub structure
978
+ const legacyDirs = [
979
+ path.join(HUB_DIR, 'system'),
980
+ path.join(HUB_DIR, '.aether'),
981
+ path.join(HUB_DIR, 'visualizations'),
982
+ ];
983
+ for (const legacyDir of legacyDirs) {
984
+ if (fs.existsSync(legacyDir)) {
985
+ try {
986
+ removeDirSync(legacyDir);
987
+ log(` Cleaned up legacy: ${path.basename(legacyDir)}/`);
988
+ } catch (err) {
989
+ // Ignore cleanup errors
990
+ }
991
+ }
992
+ }
993
+
862
994
  // Sync .claude/commands/ant/ -> ~/.aether/commands/claude/
863
995
  const claudeCmdSrc = fs.existsSync(COMMANDS_SRC)
864
996
  ? COMMANDS_SRC
@@ -894,17 +1026,6 @@ function setupHub() {
894
1026
  }
895
1027
  }
896
1028
 
897
- // Sync .aether/visualizations/ -> ~/.aether/visualizations/
898
- const visualizationsSrc = path.join(PACKAGE_DIR, '.aether', 'visualizations');
899
- if (fs.existsSync(visualizationsSrc)) {
900
- const result = syncDirWithCleanup(visualizationsSrc, HUB_VISUALIZATIONS);
901
- log(` Hub visualizations: ${result.copied} files -> ${HUB_VISUALIZATIONS}`);
902
- if (result.removed.length > 0) {
903
- log(` Hub visualizations: removed ${result.removed.length} stale files`);
904
- for (const f of result.removed) log(` - ${f}`);
905
- }
906
- }
907
-
908
1029
  // Create/preserve registry.json
909
1030
  if (!fs.existsSync(HUB_REGISTRY)) {
910
1031
  writeJsonSync(HUB_REGISTRY, { schema_version: 1, repos: [] });
@@ -971,7 +1092,7 @@ async function updateRepo(repoPath, sourceVersion, opts) {
971
1092
  }
972
1093
 
973
1094
  // Use UpdateTransaction for two-phase commit with automatic rollback
974
- const transaction = new UpdateTransaction(repoPath, { sourceVersion, quiet });
1095
+ const transaction = new UpdateTransaction(repoPath, { sourceVersion, quiet, force });
975
1096
 
976
1097
  try {
977
1098
  const result = await transaction.execute(sourceVersion, { dryRun });
@@ -980,18 +1101,15 @@ async function updateRepo(repoPath, sourceVersion, opts) {
980
1101
  const systemCopied = result.sync_result?.system?.copied || 0;
981
1102
  const commandsCopied = (result.sync_result?.commands?.copied || 0);
982
1103
  const agentsCopied = result.sync_result?.agents?.copied || 0;
983
- const visualizationsCopied = result.sync_result?.visualizations?.copied || 0;
984
1104
 
985
1105
  const systemRemoved = result.sync_result?.system?.removed?.length || 0;
986
1106
  const commandsRemoved = result.sync_result?.commands?.removed?.length || 0;
987
1107
  const agentsRemoved = result.sync_result?.agents?.removed?.length || 0;
988
- const visualizationsRemoved = result.sync_result?.visualizations?.removed?.length || 0;
989
1108
 
990
1109
  const allRemovedFiles = [
991
1110
  ...(result.sync_result?.system?.removed || []),
992
1111
  ...(result.sync_result?.commands?.removed || []).map(f => `.claude/commands/ant/${f}`),
993
1112
  ...(result.sync_result?.agents?.removed || []).map(f => `.opencode/agents/${f}`),
994
- ...(result.sync_result?.visualizations?.removed || []).map(f => `.aether/visualizations/${f}`),
995
1113
  ];
996
1114
 
997
1115
  return {
@@ -1001,8 +1119,7 @@ async function updateRepo(repoPath, sourceVersion, opts) {
1001
1119
  system: systemCopied,
1002
1120
  commands: commandsCopied,
1003
1121
  agents: agentsCopied,
1004
- visualizations: visualizationsCopied,
1005
- removed: systemRemoved + commandsRemoved + agentsRemoved + visualizationsRemoved,
1122
+ removed: systemRemoved + commandsRemoved + agentsRemoved,
1006
1123
  removedFiles: allRemovedFiles,
1007
1124
  stashCreated: !!transaction.checkpoint?.stashRef,
1008
1125
  checkpoint_id: result.checkpoint_id,
@@ -1171,14 +1288,14 @@ program
1171
1288
  console.error(` Skipping. Use --force to stash and update.`);
1172
1289
  dirty++;
1173
1290
  } else if (result.status === 'dry-run') {
1174
- log(` Would update: ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents, ${result.visualizations} visualizations]`);
1291
+ log(` Would update: ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents]`);
1175
1292
  if (result.removed > 0) {
1176
1293
  log(` Would remove ${result.removed} stale files:`);
1177
1294
  for (const f of result.removedFiles) log(` - ${f}`);
1178
1295
  }
1179
1296
  updated++;
1180
1297
  } else if (result.status === 'updated') {
1181
- log(` ${c.success('Updated:')} ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents, ${result.visualizations} visualizations]`);
1298
+ log(` ${c.success('Updated:')} ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents]`);
1182
1299
  if (result.removed > 0) {
1183
1300
  log(` Removed ${result.removed} stale files:`);
1184
1301
  for (const f of result.removedFiles) log(` - ${f}`);
@@ -1254,7 +1371,7 @@ program
1254
1371
 
1255
1372
  if (result.status === 'dry-run') {
1256
1373
  console.log(`Would update: ${result.from} -> ${result.to}`);
1257
- console.log(` ${result.system} system files, ${result.commands} command files, ${result.agents} agent files, ${result.visualizations} visualization files`);
1374
+ console.log(` ${result.system} system files, ${result.commands} command files, ${result.agents} agent files`);
1258
1375
  if (result.removed > 0) {
1259
1376
  console.log(` Would remove ${result.removed} stale files:`);
1260
1377
  for (const f of result.removedFiles) console.log(` - ${f}`);
@@ -39,6 +39,28 @@ log_error() {
39
39
  echo -e "${RED}[ERROR]${NC} $1"
40
40
  }
41
41
 
42
+ # Compute SHA hash with error handling
43
+ # Returns 0 on success, 1 on failure
44
+ # Echoes hash on success, error message on failure
45
+ compute_hash() {
46
+ local file="$1"
47
+
48
+ if [[ ! -r "$file" ]]; then
49
+ echo "NOT_READABLE"
50
+ return 1
51
+ fi
52
+
53
+ local hash
54
+ hash=$(shasum "$file" 2>/dev/null | cut -d' ' -f1)
55
+ if [[ -z "$hash" ]]; then
56
+ echo "HASH_FAILED"
57
+ return 1
58
+ fi
59
+
60
+ echo "$hash"
61
+ return 0
62
+ }
63
+
42
64
  # Count commands in each directory
43
65
  count_commands() {
44
66
  local dir="$1"
@@ -49,10 +71,17 @@ count_commands() {
49
71
  fi
50
72
  }
51
73
 
52
- # List command files
74
+ # List command files (PLAN-006 fix #13 - warn about non-.md files)
53
75
  list_commands() {
54
76
  local dir="$1"
55
77
  if [[ -d "$dir" ]]; then
78
+ # Check for non-.md files and warn
79
+ local non_md_count
80
+ non_md_count=$(find "$dir" -type f ! -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
81
+ if [[ "$non_md_count" -gt 0 ]]; then
82
+ log_warn "$non_md_count non-.md file(s) found in $dir (ignored)"
83
+ fi
84
+
56
85
  find "$dir" -name "*.md" -exec basename {} \; | sort
57
86
  fi
58
87
  }
@@ -67,6 +96,19 @@ check_sync() {
67
96
  echo "Claude Code commands: $claude_count"
68
97
  echo "OpenCode commands: $opencode_count"
69
98
 
99
+ # PLAN-006 fix #10 - warn about empty directories
100
+ if [[ "$claude_count" -eq 0 ]] && [[ "$opencode_count" -eq 0 ]]; then
101
+ log_warn "Both command directories are empty"
102
+ echo "This may indicate a misconfiguration"
103
+ fi
104
+
105
+ # PLAN-006 fix #11 - warn about large command counts
106
+ local max_commands=500
107
+ if [[ "$claude_count" -gt "$max_commands" ]] || [[ "$opencode_count" -gt "$max_commands" ]]; then
108
+ log_warn "Large number of commands ($claude_count/$opencode_count)"
109
+ echo "This may cause performance issues during sync checks"
110
+ fi
111
+
70
112
  if [[ "$claude_count" != "$opencode_count" ]]; then
71
113
  log_error "Command counts don't match!"
72
114
  return 1
@@ -96,12 +138,16 @@ check_sync() {
96
138
  check_content() {
97
139
  log_info "Checking content-level sync (checksums)..."
98
140
 
99
- local claude_files=$(list_commands "$CLAUDE_DIR")
100
141
  local drift_count=0
142
+ local error_count=0
143
+ local match_count=0
101
144
  local drift_files=""
145
+ local error_files=""
102
146
 
103
- for file in $claude_files; do
104
- local claude_file="$CLAUDE_DIR/$file"
147
+ # Use null delimiter for safe iteration (handles filenames with spaces)
148
+ while IFS= read -r -d '' claude_file; do
149
+ local file
150
+ file=$(basename "$claude_file")
105
151
  local opencode_file="$OPENCODE_DIR/$file"
106
152
 
107
153
  # Skip if OpenCode file doesn't exist (already caught by Pass 1)
@@ -109,10 +155,23 @@ check_content() {
109
155
  continue
110
156
  fi
111
157
 
112
- # Compare SHA-1 checksums (portable across macOS and Linux)
158
+ # Compute hashes with error handling
113
159
  local claude_hash opencode_hash
114
- claude_hash=$(shasum "$claude_file" | cut -d' ' -f1)
115
- opencode_hash=$(shasum "$opencode_file" | cut -d' ' -f1)
160
+ claude_hash=$(compute_hash "$claude_file")
161
+ if [[ $? -ne 0 ]]; then
162
+ log_error "Cannot hash $claude_file ($claude_hash)"
163
+ error_files="${error_files} ${file} (${claude_hash})\n"
164
+ error_count=$((error_count + 1))
165
+ continue
166
+ fi
167
+
168
+ opencode_hash=$(compute_hash "$opencode_file")
169
+ if [[ $? -ne 0 ]]; then
170
+ log_error "Cannot hash $opencode_file ($opencode_hash)"
171
+ error_files="${error_files} ${file} (${opencode_hash})\n"
172
+ error_count=$((error_count + 1))
173
+ continue
174
+ fi
116
175
 
117
176
  if [[ "$claude_hash" != "$opencode_hash" ]]; then
118
177
  drift_count=$((drift_count + 1))
@@ -122,13 +181,34 @@ check_content() {
122
181
  echo " Claude: $claude_hash"
123
182
  echo " OpenCode: $opencode_hash"
124
183
 
125
- # Show first 10 lines of diff for context (|| true to handle set -e)
184
+ # PLAN-006 fix #12 - improved diff error handling
126
185
  echo " ---"
127
- diff -u "$claude_file" "$opencode_file" | head -20 || true
186
+ local diff_output
187
+ if diff_output=$(diff -u "$claude_file" "$opencode_file" 2>&1); then
188
+ # Files are same (shouldn't happen if hashes differ, but handle it)
189
+ echo "$diff_output" | head -20
190
+ else
191
+ local diff_exit=$?
192
+ if [[ "$diff_output" == *"diff:"* && "$diff_output" == *"No such file"* ]]; then
193
+ log_error "diff failed: $diff_output"
194
+ else
195
+ # Normal diff output (exit 1 means files differ)
196
+ echo "$diff_output" | head -20
197
+ fi
198
+ fi
128
199
  echo " ---"
129
200
  echo ""
201
+ else
202
+ match_count=$((match_count + 1))
130
203
  fi
131
- done
204
+ done < <(find "$CLAUDE_DIR" -name "*.md" -type f -print0 2>/dev/null | sort -z)
205
+
206
+ # Report results
207
+ if [[ "$error_count" -gt 0 ]]; then
208
+ echo ""
209
+ log_error "Hash errors in $error_count file(s):"
210
+ echo -e "$error_files"
211
+ fi
132
212
 
133
213
  if [[ "$drift_count" -gt 0 ]]; then
134
214
  echo ""
@@ -137,7 +217,11 @@ check_content() {
137
217
  return 1
138
218
  fi
139
219
 
140
- log_info "All file contents match (checksums verified)"
220
+ if [[ "$error_count" -gt 0 ]]; then
221
+ return 1
222
+ fi
223
+
224
+ log_info "All file contents match (checksums verified: $match_count files)"
141
225
  return 0
142
226
  }
143
227
 
@@ -145,10 +229,10 @@ check_content() {
145
229
  show_diff() {
146
230
  log_info "Comparing command sets..."
147
231
 
148
- local claude_files=$(list_commands "$CLAUDE_DIR")
149
-
150
- for file in $claude_files; do
151
- local claude_file="$CLAUDE_DIR/$file"
232
+ # Use null delimiter for safe iteration (handles filenames with spaces)
233
+ while IFS= read -r -d '' claude_file; do
234
+ local file
235
+ file=$(basename "$claude_file")
152
236
  local opencode_file="$OPENCODE_DIR/$file"
153
237
 
154
238
  if [[ ! -f "$opencode_file" ]]; then
@@ -163,7 +247,7 @@ show_diff() {
163
247
  if [[ "$claude_size" != "$opencode_size" ]]; then
164
248
  echo "$file: $claude_size lines (Claude) vs $opencode_size lines (OpenCode)"
165
249
  fi
166
- done
250
+ done < <(find "$CLAUDE_DIR" -name "*.md" -type f -print0 2>/dev/null | sort -z)
167
251
  }
168
252
 
169
253
  # Display help
@@ -17,11 +17,11 @@ const pc = require('picocolors');
17
17
 
18
18
  // Caste definitions with colors and emojis (per CONTEXT.md decisions)
19
19
  const CASTE_STYLES = {
20
- builder: { color: 'blue', emoji: '🔨', ansi: '\033[34m', pc: pc.blue },
21
- watcher: { color: 'green', emoji: '👁️', ansi: '\033[32m', pc: pc.green },
22
- scout: { color: 'yellow', emoji: '🔍', ansi: '\033[33m', pc: pc.yellow },
23
- chaos: { color: 'red', emoji: '🎲', ansi: '\033[31m', pc: pc.red },
24
- prime: { color: 'magenta',emoji: '👑', ansi: '\033[35m', pc: pc.magenta }
20
+ builder: { color: 'blue', emoji: '🔨🐜', ansi: '\033[34m', pc: pc.blue },
21
+ watcher: { color: 'green', emoji: '👁️🐜', ansi: '\033[32m', pc: pc.green },
22
+ scout: { color: 'yellow', emoji: '🔍🐜', ansi: '\033[33m', pc: pc.yellow },
23
+ chaos: { color: 'red', emoji: '🎲🐜', ansi: '\033[31m', pc: pc.red },
24
+ prime: { color: 'magenta',emoji: '👑🐜', ansi: '\033[35m', pc: pc.magenta }
25
25
  };
26
26
 
27
27
  // Get style for a caste (case-insensitive)
package/bin/lib/errors.js CHANGED
@@ -179,6 +179,21 @@ class ConfigurationError extends AetherError {
179
179
  }
180
180
  }
181
181
 
182
+ /**
183
+ * StateSchemaError - State file schema validation errors (PLAN-007 Fix 3)
184
+ */
185
+ class StateSchemaError extends AetherError {
186
+ constructor(message, details = {}) {
187
+ super(
188
+ ErrorCodes.E_INVALID_STATE,
189
+ message,
190
+ details,
191
+ 'Check state file structure and fix schema errors, or restore from backup'
192
+ );
193
+ this.name = 'StateSchemaError';
194
+ }
195
+ }
196
+
182
197
  /**
183
198
  * Map error codes to sysexits.h exit codes
184
199
  * @param {string} code - Error code
@@ -233,6 +248,7 @@ module.exports = {
233
248
  ValidationError,
234
249
  FileSystemError,
235
250
  ConfigurationError,
251
+ StateSchemaError,
236
252
  ErrorCodes,
237
253
  getExitCode,
238
254
  wrapError,