@automagik/genie 0.260202.1833 → 0.260203.43

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 (108) hide show
  1. package/.beads/README.md +81 -0
  2. package/.beads/config.yaml +67 -0
  3. package/.beads/interactions.jsonl +0 -0
  4. package/.beads/issues.jsonl +9 -0
  5. package/.beads/metadata.json +4 -0
  6. package/.claude/skills/brainstorm/SKILL.md +53 -0
  7. package/.claude/skills/genie-base/SKILL.md +66 -0
  8. package/.claude/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  9. package/.claude/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  10. package/.claude/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  11. package/.claude/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  12. package/.claude/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  13. package/.claude/skills/genie-base/assets/workspace/ROLE.md +14 -0
  14. package/.claude/skills/genie-base/assets/workspace/SOUL.md +36 -0
  15. package/.claude/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  16. package/.claude/skills/genie-base/assets/workspace/USER.md +13 -0
  17. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-30.md +6 -0
  18. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-31.md +16 -0
  19. package/.claude/skills/genie-base/assets/workspace/memory/882c22be-9710-41c1-91f8-ed82947ef6ce.txt +1 -0
  20. package/.claude/skills/genie-base/scripts/install-workspace.sh +107 -0
  21. package/.claude/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  22. package/.claude/skills/genie-blank-init/SKILL.md +37 -0
  23. package/.claude/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  24. package/.claude/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  25. package/.claude/skills/genie-blank-init/assets/SOUL.md +10 -0
  26. package/.claude/skills/genie-blank-init/assets/USER.md +9 -0
  27. package/.claude/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  28. package/.claude/skills/genie-forge/SKILL.md +171 -0
  29. package/.claude/skills/genie-plan-review/CLAUDE.md +11 -0
  30. package/.claude/skills/genie-plan-review/SKILL.md +53 -0
  31. package/.claude/skills/genie-review/SKILL.md +171 -0
  32. package/.claude/skills/genie-wish/SKILL.md +141 -0
  33. package/.claude-plugin/marketplace.json +18 -0
  34. package/.genie/.gitkeep +3 -0
  35. package/.genie/backlog/hooks-v2.md +82 -0
  36. package/.genie/wishes/upgrade-brainstorm-handoff/wish.md +124 -0
  37. package/.gitattributes +3 -0
  38. package/AGENTS.md +75 -0
  39. package/bun.lock +55 -0
  40. package/dist/claudio.js +1 -1
  41. package/dist/genie.js +1 -1
  42. package/dist/term.js +123 -99
  43. package/docs/CO-ORCHESTRATION-GUIDE.md +368 -0
  44. package/package.json +5 -1
  45. package/plugin/.claude-plugin/plugin.json +18 -0
  46. package/plugin/README.md +120 -0
  47. package/plugin/agents/implementor.md +92 -0
  48. package/plugin/agents/quality-reviewer.md +113 -0
  49. package/plugin/agents/spec-reviewer.md +90 -0
  50. package/plugin/hooks/hooks.json +3 -0
  51. package/plugin/references/review-criteria.md +72 -0
  52. package/plugin/references/wish-template.md +92 -0
  53. package/plugin/scripts/genie.cjs +141 -0
  54. package/plugin/scripts/smart-install.js +308 -0
  55. package/plugin/scripts/src/install-genie-cli.sh +120 -0
  56. package/plugin/scripts/src/validate-completion.ts +142 -0
  57. package/plugin/scripts/src/validate-wish.ts +137 -0
  58. package/plugin/scripts/term.cjs +229 -0
  59. package/plugin/scripts/validate-completion.cjs +16 -0
  60. package/plugin/scripts/validate-wish.cjs +17 -0
  61. package/plugin/scripts/worker-service.cjs +28 -0
  62. package/plugin/skills/brainstorm/SKILL.md +106 -0
  63. package/plugin/skills/forge/SKILL.md +171 -0
  64. package/plugin/skills/genie-base/SKILL.md +99 -0
  65. package/plugin/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  66. package/plugin/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  67. package/plugin/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  68. package/plugin/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  69. package/plugin/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  70. package/plugin/skills/genie-base/assets/workspace/ROLE.md +14 -0
  71. package/plugin/skills/genie-base/assets/workspace/SOUL.md +36 -0
  72. package/plugin/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  73. package/plugin/skills/genie-base/assets/workspace/USER.md +13 -0
  74. package/plugin/skills/genie-base/scripts/install-workspace.sh +107 -0
  75. package/plugin/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  76. package/plugin/skills/genie-blank-init/SKILL.md +73 -0
  77. package/plugin/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  78. package/plugin/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  79. package/plugin/skills/genie-blank-init/assets/SOUL.md +10 -0
  80. package/plugin/skills/genie-blank-init/assets/USER.md +9 -0
  81. package/plugin/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  82. package/plugin/skills/genie-cli-dev/CLAUDE.md +19 -0
  83. package/plugin/skills/genie-cli-dev/SKILL.md +292 -0
  84. package/plugin/skills/plan-review/SKILL.md +101 -0
  85. package/plugin/skills/review/SKILL.md +221 -0
  86. package/plugin/skills/wish/SKILL.md +110 -0
  87. package/plugin/skills/work-orchestration/SKILL.md +110 -0
  88. package/scripts/build.js +132 -0
  89. package/scripts/smart-install.js +308 -0
  90. package/scripts/sync.js +134 -0
  91. package/src/lib/beads-registry.ts +595 -0
  92. package/src/lib/orchestrator/event-monitor.ts +2 -0
  93. package/src/lib/skill-loader.ts +215 -0
  94. package/src/lib/tmux.ts +30 -11
  95. package/src/lib/version.ts +1 -1
  96. package/src/lib/worker-registry.ts +10 -0
  97. package/src/services/worker-service.ts +351 -0
  98. package/src/term-commands/close.ts +48 -3
  99. package/src/term-commands/create.ts +95 -0
  100. package/src/term-commands/daemon.ts +176 -0
  101. package/src/term-commands/kill.ts +56 -2
  102. package/src/term-commands/orchestrate.ts +3 -2
  103. package/src/term-commands/send.ts +43 -15
  104. package/src/term-commands/spawn.ts +446 -0
  105. package/src/term-commands/split.ts +20 -8
  106. package/src/term-commands/work.ts +279 -37
  107. package/src/term-commands/workers.ts +36 -2
  108. package/src/term.ts +120 -7
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smart Install Script for automagik-genie
4
+ *
5
+ * Ensures required dependencies are installed:
6
+ * - Bun runtime (auto-installs if missing)
7
+ * - tmux (guides user if missing - can't auto-install)
8
+ * - beads (auto-installs if missing via npm)
9
+ *
10
+ * Also handles:
11
+ * - Dependency installation when version changes
12
+ * - CLI symlink creation for agent access
13
+ * - Version marker management
14
+ */
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, symlinkSync, unlinkSync } from 'fs';
16
+ import { execSync, spawnSync } from 'child_process';
17
+ import { join } from 'path';
18
+ import { homedir } from 'os';
19
+
20
+ const ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(homedir(), '.claude', 'plugins', 'automagik-genie');
21
+ const GENIE_DIR = join(homedir(), '.genie');
22
+ const MARKER = join(GENIE_DIR, '.install-version');
23
+ const BIN_DIR = join(homedir(), '.local', 'bin');
24
+ const IS_WINDOWS = process.platform === 'win32';
25
+
26
+ // Common installation paths (handles fresh installs before PATH reload)
27
+ const BUN_COMMON_PATHS = IS_WINDOWS
28
+ ? [join(homedir(), '.bun', 'bin', 'bun.exe')]
29
+ : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
30
+
31
+ const BEADS_COMMON_PATHS = IS_WINDOWS
32
+ ? [join(homedir(), 'AppData', 'Roaming', 'npm', 'bd.cmd')]
33
+ : [join(homedir(), '.local', 'bin', 'bd'), '/usr/local/bin/bd', '/opt/homebrew/bin/bd'];
34
+
35
+ /**
36
+ * Get the Bun executable path
37
+ */
38
+ function getBunPath() {
39
+ // Try PATH first
40
+ try {
41
+ const result = spawnSync('bun', ['--version'], {
42
+ encoding: 'utf-8',
43
+ stdio: ['pipe', 'pipe', 'pipe'],
44
+ shell: IS_WINDOWS
45
+ });
46
+ if (result.status === 0) return 'bun';
47
+ } catch {
48
+ // Not in PATH
49
+ }
50
+ return BUN_COMMON_PATHS.find(existsSync) || null;
51
+ }
52
+
53
+ function isBunInstalled() {
54
+ return getBunPath() !== null;
55
+ }
56
+
57
+ function getBunVersion() {
58
+ const bunPath = getBunPath();
59
+ if (!bunPath) return null;
60
+ try {
61
+ const result = spawnSync(bunPath, ['--version'], {
62
+ encoding: 'utf-8',
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ shell: IS_WINDOWS
65
+ });
66
+ return result.status === 0 ? result.stdout.trim() : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Install Bun automatically
74
+ */
75
+ function installBun() {
76
+ console.error('Installing Bun runtime...');
77
+ try {
78
+ if (IS_WINDOWS) {
79
+ execSync('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: 'inherit', shell: true });
80
+ } else {
81
+ execSync('curl -fsSL https://bun.sh/install | bash', { stdio: 'inherit', shell: true });
82
+ }
83
+ if (!isBunInstalled()) {
84
+ throw new Error('Bun installation completed but binary not found. Please restart your terminal.');
85
+ }
86
+ console.error(`Bun ${getBunVersion()} installed`);
87
+ } catch (error) {
88
+ console.error('Failed to install Bun. Please install manually:');
89
+ if (IS_WINDOWS) {
90
+ console.error(' winget install Oven-sh.Bun');
91
+ } else {
92
+ console.error(' curl -fsSL https://bun.sh/install | bash');
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if tmux is installed
100
+ */
101
+ function isTmuxInstalled() {
102
+ try {
103
+ const result = spawnSync('tmux', ['-V'], {
104
+ encoding: 'utf-8',
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ shell: IS_WINDOWS
107
+ });
108
+ return result.status === 0;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get tmux version
116
+ */
117
+ function getTmuxVersion() {
118
+ try {
119
+ const result = spawnSync('tmux', ['-V'], {
120
+ encoding: 'utf-8',
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ shell: IS_WINDOWS
123
+ });
124
+ return result.status === 0 ? result.stdout.trim() : null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Check if beads (bd) is installed
132
+ */
133
+ function getBeadsPath() {
134
+ try {
135
+ const result = spawnSync('bd', ['--version'], {
136
+ encoding: 'utf-8',
137
+ stdio: ['pipe', 'pipe', 'pipe'],
138
+ shell: IS_WINDOWS
139
+ });
140
+ if (result.status === 0) return 'bd';
141
+ } catch {
142
+ // Not in PATH
143
+ }
144
+ return BEADS_COMMON_PATHS.find(existsSync) || null;
145
+ }
146
+
147
+ function isBeadsInstalled() {
148
+ return getBeadsPath() !== null;
149
+ }
150
+
151
+ /**
152
+ * Install beads via npm
153
+ */
154
+ function installBeads() {
155
+ console.error('Installing beads (bd)...');
156
+ try {
157
+ execSync('npm install -g @anthropic-ai/bd', { stdio: 'inherit', shell: true });
158
+ if (!isBeadsInstalled()) {
159
+ throw new Error('beads installation completed but bd not found.');
160
+ }
161
+ console.error('beads installed');
162
+ } catch (error) {
163
+ console.error('Failed to install beads. Please install manually:');
164
+ console.error(' npm install -g @anthropic-ai/bd');
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if dependencies need to be installed
171
+ */
172
+ function needsInstall() {
173
+ if (!existsSync(join(ROOT, 'node_modules'))) return true;
174
+ if (!existsSync(join(ROOT, 'package.json'))) return false; // No package.json = no deps needed
175
+
176
+ try {
177
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
178
+ if (!existsSync(MARKER)) return true;
179
+ const marker = JSON.parse(readFileSync(MARKER, 'utf-8'));
180
+ return pkg.version !== marker.version || getBunVersion() !== marker.bun;
181
+ } catch {
182
+ return true;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Install dependencies using Bun
188
+ */
189
+ function installDeps() {
190
+ const bunPath = getBunPath();
191
+ if (!bunPath) {
192
+ throw new Error('Bun executable not found');
193
+ }
194
+
195
+ console.error('Installing dependencies...');
196
+
197
+ // Ensure .genie directory exists
198
+ if (!existsSync(GENIE_DIR)) {
199
+ mkdirSync(GENIE_DIR, { recursive: true });
200
+ }
201
+
202
+ const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
203
+ execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
204
+
205
+ // Write version marker
206
+ let version = 'unknown';
207
+ try {
208
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
209
+ version = pkg.version;
210
+ } catch {
211
+ // Ignore
212
+ }
213
+
214
+ writeFileSync(MARKER, JSON.stringify({
215
+ version,
216
+ bun: getBunVersion(),
217
+ tmux: getTmuxVersion(),
218
+ installedAt: new Date().toISOString()
219
+ }));
220
+ }
221
+
222
+ /**
223
+ * Create CLI symlinks for agent access
224
+ */
225
+ function createCliSymlinks() {
226
+ // Ensure bin directory exists
227
+ if (!existsSync(BIN_DIR)) {
228
+ mkdirSync(BIN_DIR, { recursive: true });
229
+ }
230
+
231
+ const links = [
232
+ { name: 'genie', target: join(ROOT, 'scripts', 'genie.cjs') },
233
+ { name: 'term', target: join(ROOT, 'scripts', 'term.cjs') }
234
+ ];
235
+
236
+ for (const { name, target } of links) {
237
+ const linkPath = join(BIN_DIR, name);
238
+
239
+ // Skip if target doesn't exist
240
+ if (!existsSync(target)) {
241
+ continue;
242
+ }
243
+
244
+ // Remove existing symlink if it exists
245
+ try {
246
+ if (existsSync(linkPath)) {
247
+ unlinkSync(linkPath);
248
+ }
249
+ } catch {
250
+ // Ignore errors
251
+ }
252
+
253
+ // Create symlink
254
+ try {
255
+ symlinkSync(target, linkPath);
256
+ console.error(`Created symlink: ${linkPath} -> ${target}`);
257
+ } catch (error) {
258
+ console.error(`Warning: Could not create symlink for ${name}: ${error.message}`);
259
+ }
260
+ }
261
+ }
262
+
263
+ // Main execution
264
+ try {
265
+ // 1. Check/install Bun
266
+ if (!isBunInstalled()) {
267
+ installBun();
268
+ }
269
+
270
+ // 2. Check tmux - REQUIRED, but can't auto-install
271
+ if (!isTmuxInstalled()) {
272
+ console.error('');
273
+ console.error('ERROR: tmux is required but not installed.');
274
+ console.error('');
275
+ console.error('Please install tmux manually:');
276
+ if (process.platform === 'darwin') {
277
+ console.error(' brew install tmux');
278
+ } else if (process.platform === 'linux') {
279
+ console.error(' sudo apt install tmux # Debian/Ubuntu');
280
+ console.error(' sudo dnf install tmux # Fedora/RHEL');
281
+ console.error(' sudo pacman -S tmux # Arch');
282
+ } else if (IS_WINDOWS) {
283
+ console.error(' WSL is required for tmux on Windows');
284
+ console.error(' Inside WSL: sudo apt install tmux');
285
+ }
286
+ console.error('');
287
+ console.error('Then restart Claude Code.');
288
+ process.exit(2); // Exit code 2 = blocking error for Claude to process
289
+ }
290
+
291
+ // 3. Check/install beads
292
+ if (!isBeadsInstalled()) {
293
+ installBeads();
294
+ }
295
+
296
+ // 4. Install dependencies if needed
297
+ if (needsInstall()) {
298
+ installDeps();
299
+ console.error('Dependencies installed');
300
+ }
301
+
302
+ // 5. Create CLI symlinks
303
+ createCliSymlinks();
304
+
305
+ } catch (e) {
306
+ console.error('Installation failed:', e.message);
307
+ process.exit(1);
308
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ #
5
+ # Install genie-cli globally via npm/bun
6
+ # This script is included with the automagik-genie plugin
7
+ #
8
+
9
+ usage() {
10
+ cat <<'EOF'
11
+ Usage:
12
+ install-genie-cli.sh [--global | --local]
13
+
14
+ Installs genie-cli, the companion CLI for the automagik-genie plugin.
15
+
16
+ Options:
17
+ --global Install globally to ~/.local/bin (default)
18
+ --local Install to current directory's node_modules
19
+
20
+ The global install creates a symlink at ~/.local/bin/genie
21
+ that points to the installed genie-cli binary.
22
+ EOF
23
+ }
24
+
25
+ INSTALL_MODE="global"
26
+
27
+ while [[ $# -gt 0 ]]; do
28
+ case "$1" in
29
+ --global)
30
+ INSTALL_MODE="global"; shift ;;
31
+ --local)
32
+ INSTALL_MODE="local"; shift ;;
33
+ -h|--help)
34
+ usage; exit 0 ;;
35
+ *)
36
+ echo "Unknown arg: $1" >&2
37
+ usage
38
+ exit 2
39
+ ;;
40
+ esac
41
+ done
42
+
43
+ # Determine plugin root (relative to this script)
44
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
45
+ GENIE_CLI_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
46
+
47
+ echo "Installing genie-cli from: $GENIE_CLI_ROOT"
48
+
49
+ # Check for package manager
50
+ if command -v bun &>/dev/null; then
51
+ PKG_MGR="bun"
52
+ elif command -v npm &>/dev/null; then
53
+ PKG_MGR="npm"
54
+ else
55
+ echo "Error: Neither bun nor npm found. Install one first." >&2
56
+ exit 1
57
+ fi
58
+
59
+ echo "Using package manager: $PKG_MGR"
60
+
61
+ if [[ "$INSTALL_MODE" == "global" ]]; then
62
+ # Global install approach: link from ~/.local/bin
63
+ mkdir -p ~/.local/bin
64
+
65
+ # Install dependencies in genie-cli directory
66
+ echo "Installing dependencies..."
67
+ cd "$GENIE_CLI_ROOT"
68
+ $PKG_MGR install
69
+
70
+ # Create symlink
71
+ GENIE_BIN="$GENIE_CLI_ROOT/src/cli.ts"
72
+ if [[ -f "$GENIE_BIN" ]]; then
73
+ # For bun, we can use the TypeScript directly
74
+ if [[ "$PKG_MGR" == "bun" ]]; then
75
+ # Create a wrapper script
76
+ cat > ~/.local/bin/genie <<EOF
77
+ #!/usr/bin/env bash
78
+ exec bun "$GENIE_BIN" "\$@"
79
+ EOF
80
+ chmod +x ~/.local/bin/genie
81
+ echo "Created wrapper at ~/.local/bin/genie"
82
+ else
83
+ # For npm, need to build first
84
+ echo "Building genie-cli..."
85
+ $PKG_MGR run build 2>/dev/null || true
86
+ if [[ -f "$GENIE_CLI_ROOT/dist/cli.js" ]]; then
87
+ ln -sf "$GENIE_CLI_ROOT/dist/cli.js" ~/.local/bin/genie
88
+ chmod +x ~/.local/bin/genie
89
+ echo "Linked to ~/.local/bin/genie"
90
+ else
91
+ echo "Warning: Build failed, using TypeScript source with bun" >&2
92
+ cat > ~/.local/bin/genie <<EOF
93
+ #!/usr/bin/env bash
94
+ exec bun "$GENIE_BIN" "\$@"
95
+ EOF
96
+ chmod +x ~/.local/bin/genie
97
+ fi
98
+ fi
99
+ else
100
+ echo "Error: genie-cli binary not found at $GENIE_BIN" >&2
101
+ exit 1
102
+ fi
103
+
104
+ echo ""
105
+ echo "genie-cli installed successfully!"
106
+ echo ""
107
+ echo "Make sure ~/.local/bin is in your PATH:"
108
+ echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
109
+ echo ""
110
+ echo "Then run: genie --help"
111
+
112
+ else
113
+ # Local install
114
+ echo "Installing genie-cli locally..."
115
+ cd "$GENIE_CLI_ROOT"
116
+ $PKG_MGR install
117
+ echo ""
118
+ echo "genie-cli installed locally."
119
+ echo "Run with: $PKG_MGR run genie --help"
120
+ fi
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Validate forge completion before session ends.
4
+ * Used by Stop hook to check if all tasks are complete.
5
+ *
6
+ * Usage: bun validate-completion.ts --session <session-id>
7
+ * bun validate-completion.ts --help
8
+ *
9
+ * This script is advisory-only (exits 0) but logs warnings if:
10
+ * - Active wish exists with incomplete tasks
11
+ * - Tasks are marked BLOCKED
12
+ */
13
+
14
+ import { parseArgs } from "util";
15
+ import { readdirSync, readFileSync, existsSync } from "fs";
16
+ import { join } from "path";
17
+
18
+ const { values } = parseArgs({
19
+ args: Bun.argv.slice(2),
20
+ options: {
21
+ session: { type: "string", short: "s" },
22
+ help: { type: "boolean", short: "h" },
23
+ },
24
+ strict: true,
25
+ });
26
+
27
+ if (values.help) {
28
+ console.log(`
29
+ validate-completion.ts - Check forge completion status
30
+
31
+ Usage:
32
+ bun validate-completion.ts --session <session-id>
33
+ bun validate-completion.ts --help
34
+
35
+ Options:
36
+ -s, --session Session ID (currently unused, for future integration)
37
+ -h, --help Show this help message
38
+
39
+ This script checks for incomplete work and logs warnings.
40
+ It always exits 0 (advisory only).
41
+ `);
42
+ process.exit(0);
43
+ }
44
+
45
+ interface WishStatus {
46
+ slug: string;
47
+ status: string;
48
+ incompleteTasks: number;
49
+ blockedTasks: number;
50
+ totalTasks: number;
51
+ }
52
+
53
+ function findWishes(baseDir: string): WishStatus[] {
54
+ const wishesDir = join(baseDir, ".genie", "wishes");
55
+ const results: WishStatus[] = [];
56
+
57
+ if (!existsSync(wishesDir)) {
58
+ return results;
59
+ }
60
+
61
+ try {
62
+ const slugs = readdirSync(wishesDir, { withFileTypes: true })
63
+ .filter((d) => d.isDirectory())
64
+ .map((d) => d.name);
65
+
66
+ for (const slug of slugs) {
67
+ const wishFile = join(wishesDir, slug, "wish.md");
68
+ if (!existsSync(wishFile)) continue;
69
+
70
+ const content = readFileSync(wishFile, "utf-8");
71
+
72
+ // Extract status
73
+ const statusMatch = content.match(/^\*\*Status:\*\*\s*(\w+)/m);
74
+ const status = statusMatch ? statusMatch[1] : "UNKNOWN";
75
+
76
+ // Skip completed wishes
77
+ if (status === "DONE") continue;
78
+
79
+ // Count execution group tasks
80
+ const groupMatches = content.match(/^###\s+Group\s+[A-Z]:/gm) || [];
81
+ const totalTasks = groupMatches.length;
82
+
83
+ // Count unchecked acceptance criteria (rough proxy for incomplete tasks)
84
+ const uncheckedCriteria = (content.match(/^-\s+\[\s+\]/gm) || []).length;
85
+ const checkedCriteria = (content.match(/^-\s+\[x\]/gim) || []).length;
86
+
87
+ // Check for BLOCKED mentions
88
+ const blockedMentions = (content.match(/BLOCKED/gi) || []).length;
89
+
90
+ results.push({
91
+ slug,
92
+ status,
93
+ incompleteTasks: uncheckedCriteria > 0 ? Math.ceil(uncheckedCriteria / 3) : 0, // Rough estimate
94
+ blockedTasks: blockedMentions > 0 ? 1 : 0,
95
+ totalTasks,
96
+ });
97
+ }
98
+ } catch (error) {
99
+ // Silent failure - don't block session end
100
+ }
101
+
102
+ return results;
103
+ }
104
+
105
+ // Find wishes from current directory
106
+ const cwd = process.cwd();
107
+ const wishes = findWishes(cwd);
108
+
109
+ // Filter to active wishes (IN_PROGRESS)
110
+ const activeWishes = wishes.filter((w) => w.status === "IN_PROGRESS");
111
+
112
+ if (activeWishes.length === 0) {
113
+ // No active wishes, nothing to report
114
+ process.exit(0);
115
+ }
116
+
117
+ // Log warnings for incomplete work
118
+ let hasWarnings = false;
119
+
120
+ for (const wish of activeWishes) {
121
+ if (wish.incompleteTasks > 0 || wish.blockedTasks > 0) {
122
+ hasWarnings = true;
123
+ console.log(`\n⚠ Active wish "${wish.slug}" has incomplete work:`);
124
+
125
+ if (wish.incompleteTasks > 0) {
126
+ console.log(` - ~${wish.incompleteTasks} tasks with unchecked criteria`);
127
+ }
128
+
129
+ if (wish.blockedTasks > 0) {
130
+ console.log(` - ${wish.blockedTasks} BLOCKED task(s) need attention`);
131
+ }
132
+
133
+ console.log(` Run /forge to continue or /review to validate.`);
134
+ }
135
+ }
136
+
137
+ if (hasWarnings) {
138
+ console.log("");
139
+ }
140
+
141
+ // Always exit 0 (advisory only)
142
+ process.exit(0);
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Validate a wish document structure before writing.
4
+ * Used by PreToolUse hook to catch missing sections.
5
+ *
6
+ * Usage: bun validate-wish.ts --file <path-to-wish.md>
7
+ * bun validate-wish.ts --help
8
+ */
9
+
10
+ import { parseArgs } from "util";
11
+ import { readFileSync, existsSync } from "fs";
12
+
13
+ const { values } = parseArgs({
14
+ args: Bun.argv.slice(2),
15
+ options: {
16
+ file: { type: "string", short: "f" },
17
+ help: { type: "boolean", short: "h" },
18
+ },
19
+ strict: true,
20
+ });
21
+
22
+ if (values.help) {
23
+ console.log(`
24
+ validate-wish.ts - Validate wish document structure
25
+
26
+ Usage:
27
+ bun validate-wish.ts --file <path-to-wish.md>
28
+ bun validate-wish.ts --help
29
+
30
+ Options:
31
+ -f, --file Path to wish document to validate
32
+ -h, --help Show this help message
33
+
34
+ Exit codes:
35
+ 0 Validation passed
36
+ 1 Validation failed (missing required sections)
37
+ 2 Invalid arguments or file not found
38
+ `);
39
+ process.exit(0);
40
+ }
41
+
42
+ if (!values.file) {
43
+ console.error("Error: --file is required");
44
+ process.exit(2);
45
+ }
46
+
47
+ if (!existsSync(values.file)) {
48
+ // File doesn't exist yet (being created), skip validation
49
+ console.log("File not found, skipping validation (new file)");
50
+ process.exit(0);
51
+ }
52
+
53
+ const content = readFileSync(values.file, "utf-8");
54
+
55
+ interface ValidationResult {
56
+ passed: boolean;
57
+ issues: string[];
58
+ }
59
+
60
+ function validateWish(content: string): ValidationResult {
61
+ const issues: string[] = [];
62
+
63
+ // Required sections
64
+ const requiredSections = [
65
+ { pattern: /^##\s+Summary/m, name: "## Summary" },
66
+ { pattern: /^##\s+Scope/m, name: "## Scope" },
67
+ { pattern: /^###\s+IN/m, name: "### IN (under Scope)" },
68
+ { pattern: /^###\s+OUT/m, name: "### OUT (under Scope)" },
69
+ { pattern: /^##\s+Success Criteria/m, name: "## Success Criteria" },
70
+ { pattern: /^##\s+Execution Groups/m, name: "## Execution Groups" },
71
+ ];
72
+
73
+ for (const { pattern, name } of requiredSections) {
74
+ if (!pattern.test(content)) {
75
+ issues.push(`Missing required section: ${name}`);
76
+ }
77
+ }
78
+
79
+ // Check for at least one execution group
80
+ const groupPattern = /^###\s+Group\s+[A-Z]:/m;
81
+ if (!groupPattern.test(content)) {
82
+ issues.push("Missing execution group (need at least one ### Group X: section)");
83
+ }
84
+
85
+ // Check for acceptance criteria in groups
86
+ const groups = content.match(/^###\s+Group\s+[A-Z]:.*/gm) || [];
87
+ if (groups.length > 0) {
88
+ // Check if Acceptance Criteria exists somewhere after Execution Groups
89
+ const execGroupsIndex = content.indexOf("## Execution Groups");
90
+ const afterExecGroups = content.slice(execGroupsIndex);
91
+
92
+ if (!afterExecGroups.includes("**Acceptance Criteria:**")) {
93
+ issues.push("Execution groups should have **Acceptance Criteria:** sections");
94
+ }
95
+
96
+ if (!afterExecGroups.includes("**Validation:**")) {
97
+ issues.push("Execution groups should have **Validation:** command sections");
98
+ }
99
+ }
100
+
101
+ // Check that OUT scope is not empty
102
+ const outMatch = content.match(/^###\s+OUT\s*\n([\s\S]*?)(?=^##|^###|\n---|\Z)/m);
103
+ if (outMatch) {
104
+ const outContent = outMatch[1].trim();
105
+ // Check if it only has placeholder text or is empty
106
+ if (!outContent || outContent === "-" || outContent.match(/^-\s*$/)) {
107
+ issues.push("OUT scope should not be empty - add explicit exclusions");
108
+ }
109
+ }
110
+
111
+ // Check for success criteria checkboxes
112
+ const successSection = content.match(/^##\s+Success Criteria\s*\n([\s\S]*?)(?=^##|\n---|\Z)/m);
113
+ if (successSection) {
114
+ const checkboxes = successSection[1].match(/^-\s+\[\s*\]/gm) || [];
115
+ if (checkboxes.length === 0) {
116
+ issues.push("Success Criteria should have checkbox items (- [ ])");
117
+ }
118
+ }
119
+
120
+ return {
121
+ passed: issues.length === 0,
122
+ issues,
123
+ };
124
+ }
125
+
126
+ const result = validateWish(content);
127
+
128
+ if (result.passed) {
129
+ console.log("✓ Wish document validation passed");
130
+ process.exit(0);
131
+ } else {
132
+ console.log("⚠ Wish document validation issues:");
133
+ for (const issue of result.issues) {
134
+ console.log(` - ${issue}`);
135
+ }
136
+ process.exit(1);
137
+ }