@automagik/genie 4.260325.4 → 4.260325.6

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260325.4",
13
+ "version": "4.260325.6",
14
14
  "source": "./plugins/genie",
15
15
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
16
16
  }
@@ -25,6 +25,9 @@ Update README.md with live project metrics daily. After each run, analyze perfor
25
25
  | Releases/day | `gh api repos/{owner}/{repo}/releases` | Count releases created in last 24h |
26
26
  | Avg bug-fix time | `gh api repos/{owner}/{repo}/pulls?state=closed` | Mean time from PR open → merge for bug-fix PRs (last 7 days) |
27
27
  | SHIP rate | `gh api repos/{owner}/{repo}/pulls?state=closed` | % of PRs that shipped without FIX-FIRST (merged on first review cycle) |
28
+ | Lines changed (24h) | `git log --since="24 hours ago" --stat` | Total insertions + deletions in the last 24 hours |
29
+ | Commits (24h) | `git log --since="24 hours ago" --oneline` | Count of commits in the last 24 hours |
30
+ | Pull requests (24h) | `gh api search/issues` | Count of PRs created in the last 24 hours |
28
31
  | Parallel agents | `genie status` or process count | Number of active genie workers at time of run |
29
32
 
30
33
  ## Execution Steps
@@ -79,20 +82,29 @@ After the metrics update completes:
79
82
 
80
83
  ## README Metrics Table Format
81
84
 
82
- Insert after the badges block, before "## What is Genie?":
85
+ Insert after the badges block, before "## What is Genie?". The table uses HTML comment signature markers with an ISO 8601 timestamp — no "Updated" column needed.
83
86
 
84
87
  ```markdown
85
- <!-- METRICS:START -->
88
+ <!-- METRICS:START — Updated by Genie Metrics Agent at 2026-03-24T23:45:00Z -->
89
+ | Metric | Value |
90
+ |--------|-------|
91
+ | Releases/day | 17 |
92
+ | Avg bug-fix time | 1.7h |
93
+ | SHIP rate | 100% |
94
+ | Lines changed (24h) | 12,450 |
95
+ | Commits (24h) | 34 |
96
+ | Pull requests (24h) | 8 |
97
+ | Parallel agents | 5 |
98
+ <!-- METRICS:END — 🧞 automagik/genie -->
99
+ ```
86
100
 
87
- | Metric | Value | Updated |
88
- |--------|-------|---------|
89
- | Releases/day | **X** | YYYY-MM-DD |
90
- | Avg bug-fix time | **Xh** | YYYY-MM-DD |
91
- | SHIP rate | **X%** | YYYY-MM-DD |
92
- | Parallel agents | **X** | YYYY-MM-DD |
101
+ ### Quality Expectations
93
102
 
94
- <!-- METRICS:END -->
95
- ```
103
+ - The table MUST have exactly 7 metric rows — no more, no less
104
+ - Values MUST be real data from GitHub API and git log — never hardcoded placeholders
105
+ - The ISO timestamp in the START marker MUST reflect the actual update time in UTC
106
+ - The bottom marker includes the Genie signature (`🧞 automagik/genie`) for attribution
107
+ - Numbers should be human-readable: use comma separators for LoC (e.g., `12,450`)
96
108
 
97
109
  ## Tools Available
98
110
 
@@ -173,7 +185,7 @@ The refined prompt replaces this file for the next run. Tools persist in `tools/
173
185
 
174
186
  If GitHub API is unavailable:
175
187
  1. Read `state.json` for `last_metrics`
176
- 2. Use yesterday's values in README (do not update the "Updated" column)
188
+ 2. Use yesterday's values in README (do not update the timestamp in the START marker)
177
189
  3. Log the error to `runs.jsonl` with step timings
178
190
  4. Skip commit (no changes to README)
179
191
  5. Still call `/refine` to analyze the failure and improve error handling
@@ -7,3 +7,4 @@
7
7
  {"timestamp": "2026-03-24T22:57:30Z", "duration_ms": 4728, "api_calls": 2, "tools_generated": 0, "tools_available": 10, "errors": [], "status": "no_changes", "fallback": false, "slowest_step": "fetch_releases", "steps": [{"name": "load_state", "duration_ms": 31}, {"name": "fetch_releases", "duration_ms": 3211}, {"name": "fetch_prs", "duration_ms": 1186}, {"name": "count_agents", "duration_ms": 59}, {"name": "parse_metrics", "duration_ms": 126}, {"name": "update_readme", "duration_ms": 52}, {"name": "update_state", "duration_ms": 34}, {"name": "commit", "duration_ms": 3}], "metrics": {"releases_per_day": 0, "avg_bugfix_time_hours": 1.9, "ship_rate_pct": 100.0, "parallel_agents": 3, "updated": "2026-03-24"}}
8
8
  {"timestamp": "2026-03-24T22:57:37Z", "duration_ms": 4374, "api_calls": 2, "tools_generated": 0, "tools_available": 10, "errors": [], "status": "no_changes", "fallback": false, "slowest_step": "fetch_releases", "steps": [{"name": "load_state", "duration_ms": 34}, {"name": "fetch_releases", "duration_ms": 2985}, {"name": "fetch_prs", "duration_ms": 1063}, {"name": "count_agents", "duration_ms": 53}, {"name": "parse_metrics", "duration_ms": 131}, {"name": "update_readme", "duration_ms": 47}, {"name": "update_state", "duration_ms": 34}, {"name": "commit", "duration_ms": 3}], "metrics": {"releases_per_day": 0, "avg_bugfix_time_hours": 1.9, "ship_rate_pct": 100.0, "parallel_agents": 3, "updated": "2026-03-24"}}
9
9
  {"timestamp": "2026-03-24T22:57:43Z", "duration_ms": 4201, "api_calls": 2, "tools_generated": 0, "tools_available": 10, "errors": [], "status": "no_changes", "fallback": false, "slowest_step": "fetch_releases", "steps": [{"name": "load_state", "duration_ms": 32}, {"name": "fetch_releases", "duration_ms": 2704}, {"name": "fetch_prs", "duration_ms": 1172}, {"name": "count_agents", "duration_ms": 65}, {"name": "parse_metrics", "duration_ms": 122}, {"name": "update_readme", "duration_ms": 43}, {"name": "update_state", "duration_ms": 35}, {"name": "commit", "duration_ms": 3}], "metrics": {"releases_per_day": 0, "avg_bugfix_time_hours": 1.9, "ship_rate_pct": 100.0, "parallel_agents": 3, "updated": "2026-03-24"}}
10
+ {"timestamp": "2026-03-25T00:03:05Z", "duration_ms": 5882, "api_calls": 2, "tools_generated": 0, "tools_available": 10, "errors": [], "status": "success", "fallback": false, "slowest_step": "fetch_releases", "steps": [{"name": "load_state", "duration_ms": 33}, {"name": "fetch_releases", "duration_ms": 3458}, {"name": "fetch_prs", "duration_ms": 1046}, {"name": "count_agents", "duration_ms": 53}, {"name": "parse_metrics", "duration_ms": 1191}, {"name": "update_readme", "duration_ms": 45}, {"name": "update_state", "duration_ms": 30}, {"name": "commit", "duration_ms": 4}], "metrics": {"releases_per_day": 0, "avg_bugfix_time_hours": 1.7, "ship_rate_pct": 100.0, "loc_changed_24h": 7418, "commits_24h": 66, "prs_24h": 28, "parallel_agents": 3, "updated": "2026-03-25T00:03:04Z"}}
@@ -1,21 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
  # commit-formatter.sh — Clean commit message formatter for metrics updates
3
3
  # Auto-generated by metrics-updater agent (v1, 2026-03-24)
4
- # This tool will be refined by the agent over time.
4
+ # Updated: v2 includes LoC/commits/PRs in commit message
5
5
 
6
6
  set -euo pipefail
7
7
 
8
8
  # Usage: commit-formatter.sh <metrics_json_file>
9
9
  # Reads metrics JSON and outputs a formatted commit message.
10
- #
11
- # Input JSON format:
12
- # {
13
- # "releases_per_day": 27,
14
- # "avg_bugfix_time_hours": 2.4,
15
- # "ship_rate_pct": 100,
16
- # "parallel_agents": 5,
17
- # "updated": "2026-03-24"
18
- # }
19
10
 
20
11
  METRICS_FILE="${1:-}"
21
12
 
@@ -28,7 +19,9 @@ fi
28
19
  releases=$(jq -r '.releases_per_day // 0' "$METRICS_FILE")
29
20
  avg_time=$(jq -r '.avg_bugfix_time_hours // 0' "$METRICS_FILE")
30
21
  ship_rate=$(jq -r '.ship_rate_pct // 0' "$METRICS_FILE")
31
- agents=$(jq -r '.parallel_agents // 0' "$METRICS_FILE")
22
+ loc=$(jq -r '.loc_changed_24h // 0' "$METRICS_FILE")
23
+ commits=$(jq -r '.commits_24h // 0' "$METRICS_FILE")
24
+ prs=$(jq -r '.prs_24h // 0' "$METRICS_FILE")
32
25
 
33
26
  # Format commit message
34
- echo "chore: update live metrics (${releases}/day, ${avg_time}h avg, ${ship_rate}% SHIP)"
27
+ echo "chore: update live metrics (${releases}/day, ${avg_time}h avg, ${ship_rate}% SHIP, ${loc} LoC, ${commits} commits, ${prs} PRs)"
@@ -2,14 +2,15 @@
2
2
  """parse-metrics.py — Metrics parser and calculator for metrics-updater agent.
3
3
 
4
4
  Auto-generated by metrics-updater agent (v1, 2026-03-24).
5
- This tool will be refined by the agent over time.
5
+ Updated: v2 added LoC, commits, PRs (24h) metrics.
6
6
 
7
7
  Usage:
8
- python3 parse-metrics.py --releases-json <file> --prs-json <file> [--state-json <file>]
8
+ python3 parse-metrics.py --releases-json <file> --prs-json <file> [--repo-root <path>]
9
9
  python3 parse-metrics.py --from-state <state.json> # fallback mode
10
10
  """
11
11
 
12
12
  import json
13
+ import subprocess
13
14
  import sys
14
15
  import argparse
15
16
  from datetime import datetime, timedelta, timezone
@@ -68,16 +69,69 @@ def calc_ship_rate(prs_json: list, days: int = 7) -> float:
68
69
  return round((shipped_first / total) * 100, 0)
69
70
 
70
71
 
72
+ def calc_loc_changed(repo_root: str) -> int:
73
+ """Calculate total lines changed (additions + deletions) in last 24 hours."""
74
+ try:
75
+ result = subprocess.run(
76
+ ['git', 'log', '--since=24 hours ago', '--stat', '--format='],
77
+ capture_output=True, text=True, cwd=repo_root
78
+ )
79
+ total = 0
80
+ for line in result.stdout.splitlines():
81
+ if 'file' in line and 'changed' in line:
82
+ parts = line.split(',')
83
+ for part in parts:
84
+ part = part.strip()
85
+ if 'insertion' in part:
86
+ total += int(part.split()[0])
87
+ elif 'deletion' in part:
88
+ total += int(part.split()[0])
89
+ return total
90
+ except Exception:
91
+ return 0
92
+
93
+
94
+ def calc_commits_24h(repo_root: str) -> int:
95
+ """Count commits in the last 24 hours."""
96
+ try:
97
+ result = subprocess.run(
98
+ ['git', 'log', '--since=24 hours ago', '--oneline'],
99
+ capture_output=True, text=True, cwd=repo_root
100
+ )
101
+ lines = [l for l in result.stdout.strip().splitlines() if l.strip()]
102
+ return len(lines)
103
+ except Exception:
104
+ return 0
105
+
106
+
107
+ def calc_prs_24h(owner: str, repo: str) -> int:
108
+ """Count PRs created in the last 24 hours using gh API."""
109
+ try:
110
+ since = (datetime.now(timezone.utc) - timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%SZ')
111
+ result = subprocess.run(
112
+ ['gh', 'api', f'search/issues?q=repo:{owner}/{repo}+type:pr+created:>={since}',
113
+ '--jq', '.total_count'],
114
+ capture_output=True, text=True
115
+ )
116
+ return int(result.stdout.strip()) if result.stdout.strip() else 0
117
+ except Exception:
118
+ return 0
119
+
120
+
71
121
  def format_metrics(releases_per_day: int, avg_merge_hours: float,
72
- ship_rate: float, parallel_agents: int) -> dict:
122
+ ship_rate: float, parallel_agents: int,
123
+ loc_changed_24h: int = 0, commits_24h: int = 0,
124
+ prs_24h: int = 0) -> dict:
73
125
  """Format metrics into the standard output structure."""
74
- today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
75
126
  return {
76
127
  'releases_per_day': releases_per_day,
77
128
  'avg_bugfix_time_hours': avg_merge_hours,
78
129
  'ship_rate_pct': ship_rate,
130
+ 'loc_changed_24h': loc_changed_24h,
131
+ 'commits_24h': commits_24h,
132
+ 'prs_24h': prs_24h,
79
133
  'parallel_agents': parallel_agents,
80
- 'updated': today,
134
+ 'updated': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
81
135
  }
82
136
 
83
137
 
@@ -87,6 +141,9 @@ def main():
87
141
  parser.add_argument('--prs-json', help='Path to PRs JSON file')
88
142
  parser.add_argument('--parallel-agents', type=int, default=0,
89
143
  help='Number of parallel agents active')
144
+ parser.add_argument('--repo-root', default='.', help='Path to repo root for git log commands')
145
+ parser.add_argument('--owner', default='automagik-dev', help='GitHub owner')
146
+ parser.add_argument('--repo', default='genie', help='GitHub repo name')
90
147
  parser.add_argument('--from-state', help='Fallback: read last_metrics from state.json')
91
148
  parser.add_argument('--output', '-o', help='Output file (default: stdout)')
92
149
 
@@ -112,12 +169,18 @@ def main():
112
169
  releases_count = parse_releases_count(releases)
113
170
  avg_merge = calc_avg_merge_time_hours(prs)
114
171
  ship_rate = calc_ship_rate(prs)
172
+ loc_changed = calc_loc_changed(args.repo_root)
173
+ commits = calc_commits_24h(args.repo_root)
174
+ prs_count = calc_prs_24h(args.owner, args.repo)
115
175
 
116
176
  metrics = format_metrics(
117
177
  releases_per_day=releases_count,
118
178
  avg_merge_hours=avg_merge,
119
179
  ship_rate=ship_rate,
120
180
  parallel_agents=args.parallel_agents,
181
+ loc_changed_24h=loc_changed,
182
+ commits_24h=commits,
183
+ prs_24h=prs_count,
121
184
  )
122
185
 
123
186
  output = json.dumps(metrics, indent=2)
@@ -149,6 +149,9 @@ if [[ "$FETCH_OK" == "true" ]]; then
149
149
  --releases-json "$RELEASES_JSON" \
150
150
  --prs-json "$PRS_JSON" \
151
151
  --parallel-agents "$PARALLEL_AGENTS" \
152
+ --repo-root "$REPO_ROOT" \
153
+ --owner "$OWNER" \
154
+ --repo "$REPO" \
152
155
  -o "$METRICS_JSON"; then
153
156
  log "Metrics calculated OK"
154
157
  else
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """update-readme.py — Update README.md metrics table between sentinel markers.
2
+ """update-readme.py — Update README.md metrics table between signature markers.
3
3
 
4
4
  Auto-generated by metrics-updater agent (v1, 2026-03-24).
5
+ Updated: v2 — ISO timestamp signature, 7 metric rows, no Updated column.
5
6
 
6
7
  Usage:
7
8
  python3 update-readme.py --metrics <metrics.json> --readme <README.md>
@@ -12,30 +13,37 @@ import json
12
13
  import re
13
14
  import sys
14
15
  import argparse
16
+ from datetime import datetime, timezone
15
17
  from pathlib import Path
16
18
 
17
- START_MARKER = '<!-- METRICS:START -->'
18
- END_MARKER = '<!-- METRICS:END -->'
19
+ START_MARKER_PREFIX = '<!-- METRICS:START'
20
+ END_MARKER = '<!-- METRICS:END \u2014 \U0001f9de automagik/genie -->'
19
21
 
20
22
 
21
23
  def build_metrics_table(metrics: dict) -> str:
22
- """Build the markdown metrics table from metrics dict."""
23
- updated = metrics.get('updated', 'N/A')
24
+ """Build the markdown metrics table with signature markers."""
25
+ timestamp = metrics.get('updated', datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'))
24
26
  releases = metrics.get('releases_per_day', 0)
25
27
  avg_time = metrics.get('avg_bugfix_time_hours', 0)
26
28
  ship_rate = metrics.get('ship_rate_pct', 0)
29
+ loc_changed = metrics.get('loc_changed_24h', 0)
30
+ commits = metrics.get('commits_24h', 0)
31
+ prs = metrics.get('prs_24h', 0)
27
32
  agents = metrics.get('parallel_agents', 0)
28
33
 
34
+ start_marker = f'<!-- METRICS:START \u2014 Updated by Genie Metrics Agent at {timestamp} -->'
35
+
29
36
  lines = [
30
- START_MARKER,
31
- '',
32
- '| Metric | Value | Updated |',
33
- '|--------|-------|---------|',
34
- f'| Releases/day | **{releases}** | {updated} |',
35
- f'| Avg bug-fix time | **{avg_time}h** | {updated} |',
36
- f'| SHIP rate | **{ship_rate}%** | {updated} |',
37
- f'| Parallel agents | **{agents}** | {updated} |',
38
- '',
37
+ start_marker,
38
+ '| Metric | Value |',
39
+ '|--------|-------|',
40
+ f'| Releases/day | {releases} |',
41
+ f'| Avg bug-fix time | {avg_time}h |',
42
+ f'| SHIP rate | {ship_rate}% |',
43
+ f'| Lines changed (24h) | {loc_changed:,} |',
44
+ f'| Commits (24h) | {commits} |',
45
+ f'| Pull requests (24h) | {prs} |',
46
+ f'| Parallel agents | {agents} |',
39
47
  END_MARKER,
40
48
  ]
41
49
  return '\n'.join(lines)
@@ -48,11 +56,11 @@ def update_readme(readme_path: str, metrics: dict) -> bool:
48
56
 
49
57
  new_table = build_metrics_table(metrics)
50
58
 
51
- # Check if markers exist
52
- if START_MARKER in content and END_MARKER in content:
53
- # Replace existing block
59
+ # Check if markers exist (match any START marker variant)
60
+ if START_MARKER_PREFIX in content and ('<!-- METRICS:END' in content):
61
+ # Replace existing block (match old or new marker formats)
54
62
  pattern = re.compile(
55
- re.escape(START_MARKER) + r'.*?' + re.escape(END_MARKER),
63
+ r'<!-- METRICS:START[^\n]*-->.*?<!-- METRICS:END[^\n]*-->',
56
64
  re.DOTALL,
57
65
  )
58
66
  new_content = pattern.sub(new_table, content)
package/dist/genie.js CHANGED
@@ -159,8 +159,8 @@ ${errCtx.stack}`;d.reject(err)}else d.resolve(msg)}});return sub.requestSubject=
159
159
  tell.location
160
160
  from (select lo_tell64($1) as location) tell
161
161
  ) seek
162
- `};return resolve4(lo),new Promise(async(r)=>finish=r);async function readable({highWaterMark=16384,start=0,end=1/0}={}){let max=end-start;return start&&await lo.seek(start),new Stream2.Readable({highWaterMark,async read(size){let l=size>max?size-max:size;max-=size;let[{data}]=await lo.read(l);if(this.push(data),data.length<size)this.push(null)}})}async function writable({highWaterMark=16384,start=0}={}){return start&&await lo.seek(start),new Stream2.Writable({highWaterMark,write(chunk,encoding,callback){lo.write(chunk).then(()=>callback(),callback)}})}}).catch(reject)})}var init_large=()=>{};var exports_src={};__export(exports_src,{default:()=>src_default});import os from"os";import fs from"fs";function Postgres(a,b2){let options=parseOptions(a,b2),subscribe2=options.no_subscribe||Subscribe(Postgres,{...options}),ending=!1,queries=queue_default(),connecting=queue_default(),reserved=queue_default(),closed=queue_default(),ended=queue_default(),open2=queue_default(),busy=queue_default(),full=queue_default(),queues={connecting,reserved,closed,ended,open:open2,busy,full},connections=[...Array(options.max)].map(()=>connection_default(options,queues,{onopen,onend,onclose})),sql=Sql(handler);return Object.assign(sql,{get parameters(){return options.parameters},largeObject:largeObject.bind(null,sql),subscribe:subscribe2,CLOSE,END:CLOSE,PostgresError,options,reserve,listen,begin,close:close2,end}),sql;function Sql(handler2){return handler2.debug=options.debug,Object.entries(options.types).reduce((acc,[name,type2])=>{return acc[name]=(x)=>new Parameter(x,type2.to),acc},typed),Object.assign(sql2,{types:typed,typed,unsafe,notify,array,json:json2,file}),sql2;function typed(value,type2){return new Parameter(value,type2)}function sql2(strings,...args){return strings&&Array.isArray(strings.raw)?new Query(strings,args,handler2,cancel):typeof strings==="string"&&!args.length?new Identifier(options.transform.column.to?options.transform.column.to(strings):strings):new Builder(strings,args)}function unsafe(string,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([string],args,handler2,cancel,{prepare:!1,...options2,simple:"simple"in options2?options2.simple:args.length===0})}function file(path2,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([],args,(query2)=>{fs.readFile(path2,"utf8",(err,string)=>{if(err)return query2.reject(err);query2.strings=[string],handler2(query2)})},cancel,{...options2,simple:"simple"in options2?options2.simple:args.length===0})}}async function listen(name,fn,onlisten){let listener={fn,onlisten},sql2=listen.sql||(listen.sql=Postgres({...options,max:1,idle_timeout:null,max_lifetime:null,fetch_types:!1,onclose(){Object.entries(listen.channels).forEach(([name2,{listeners}])=>{delete listen.channels[name2],Promise.all(listeners.map((l)=>listen(name2,l.fn,l.onlisten).catch(()=>{})))})},onnotify(c,x){c in listen.channels&&listen.channels[c].listeners.forEach((l)=>l.fn(x))}})),channels=listen.channels||(listen.channels={});if(name in channels){channels[name].listeners.push(listener);let result2=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result2.state,unlisten}}channels[name]={result:sql2`listen ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`,listeners:[listener]};let result=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result.state,unlisten};async function unlisten(){if(name in channels===!1)return;if(channels[name].listeners=channels[name].listeners.filter((x)=>x!==listener),channels[name].listeners.length)return;return delete channels[name],sql2`unlisten ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`}}async function notify(channel,payload){return await sql`select pg_notify(${channel}, ${""+payload})`}async function reserve(){let queue=queue_default(),c=open2.length?open2.shift():await new Promise((resolve4,reject)=>{let query={reserve:resolve4,reject};queries.push(query),closed.length&&connect(closed.shift(),query)});move(c,reserved),c.reserved=()=>queue.length?c.execute(queue.shift()):move(c,reserved),c.reserved.release=!0;let sql2=Sql(handler2);return sql2.release=()=>{c.reserved=null,onopen(c)},sql2;function handler2(q){c.queue===full?queue.push(q):c.execute(q)||move(c,full)}}async function begin(options2,fn){!fn&&(fn=options2,options2="");let queries2=queue_default(),savepoints=0,connection2,prepare=null;try{return await sql.unsafe("begin "+options2.replace(/[^a-z ]/ig,""),[],{onexecute}).execute(),await Promise.race([scope(connection2,fn),new Promise((_,reject)=>connection2.onclose=reject)])}catch(error2){throw error2}async function scope(c,fn2,name){let sql2=Sql(handler2);sql2.savepoint=savepoint,sql2.prepare=(x)=>prepare=x.replace(/[^a-z0-9$-_. ]/gi);let uncaughtError,result;name&&await sql2`savepoint ${sql2(name)}`;try{if(result=await new Promise((resolve4,reject)=>{let x=fn2(sql2);Promise.resolve(Array.isArray(x)?Promise.all(x):x).then(resolve4,reject)}),uncaughtError)throw uncaughtError}catch(e){throw await(name?sql2`rollback to ${sql2(name)}`:sql2`rollback`),e instanceof PostgresError&&e.code==="25P02"&&uncaughtError||e}if(!name)prepare?await sql2`prepare transaction '${sql2.unsafe(prepare)}'`:await sql2`commit`;return result;function savepoint(name2,fn3){if(name2&&Array.isArray(name2.raw))return savepoint((sql3)=>sql3.apply(sql3,arguments));return arguments.length===1&&(fn3=name2,name2=null),scope(c,fn3,"s"+savepoints+++(name2?"_"+name2:""))}function handler2(q){q.catch((e)=>uncaughtError||(uncaughtError=e)),c.queue===full?queries2.push(q):c.execute(q)||move(c,full)}}function onexecute(c){connection2=c,move(c,reserved),c.reserved=()=>queries2.length?c.execute(queries2.shift()):move(c,reserved)}}function move(c,queue){return c.queue.remove(c),queue.push(c),c.queue=queue,queue===open2?c.idleTimer.start():c.idleTimer.cancel(),c}function json2(x){return new Parameter(x,3802)}function array(x,type2){if(!Array.isArray(x))return array(Array.from(arguments));return new Parameter(x,type2||(x.length?inferType(x)||25:0),options.shared.typeArrayMap)}function handler(query){if(ending)return query.reject(Errors.connection("CONNECTION_ENDED",options,options));if(open2.length)return go(open2.shift(),query);if(closed.length)return connect(closed.shift(),query);busy.length?go(busy.shift(),query):queries.push(query)}function go(c,query){return c.execute(query)?move(c,busy):move(c,full)}function cancel(query){return new Promise((resolve4,reject)=>{query.state?query.active?connection_default(options).cancel(query.state,resolve4,reject):query.cancelled={resolve:resolve4,reject}:(queries.remove(query),query.cancelled=!0,query.reject(Errors.generic("57014","canceling statement due to user request")),resolve4())})}async function end({timeout=null}={}){if(ending)return ending;await 1;let timer2;return ending=Promise.race([new Promise((r)=>timeout!==null&&(timer2=setTimeout(destroy,timeout*1000,r))),Promise.all(connections.map((c)=>c.end()).concat(listen.sql?listen.sql.end({timeout:0}):[],subscribe2.sql?subscribe2.sql.end({timeout:0}):[]))]).then(()=>clearTimeout(timer2))}async function close2(){await Promise.all(connections.map((c)=>c.end()))}async function destroy(resolve4){await Promise.all(connections.map((c)=>c.terminate()));while(queries.length)queries.shift().reject(Errors.connection("CONNECTION_DESTROYED",options));resolve4()}function connect(c,query){return move(c,connecting),c.connect(query),c}function onend(c){move(c,ended)}function onopen(c){if(queries.length===0)return move(c,open2);let max=Math.ceil(queries.length/(connecting.length+1)),ready=!0;while(ready&&queries.length&&max-- >0){let query=queries.shift();if(query.reserve)return query.reserve(c);ready=c.execute(query)}ready?move(c,busy):move(c,full)}function onclose(c,e){move(c,closed),c.reserved=null,c.onclose&&(c.onclose(e),c.onclose=null),options.onclose&&options.onclose(c.id),queries.length&&connect(c,queries.shift())}}function parseOptions(a,b2){if(a&&a.shared)return a;let env=process.env,o=(!a||typeof a==="string"?b2:a)||{},{url,multihost}=parseUrl(a),query=[...url.searchParams].reduce((a2,[b3,c])=>(a2[b3]=c,a2),{}),host=o.hostname||o.host||multihost||url.hostname||env.PGHOST||"localhost",port=o.port||url.port||env.PGPORT||5432,user=o.user||o.username||url.username||env.PGUSERNAME||env.PGUSER||osUsername();o.no_prepare&&(o.prepare=!1),query.sslmode&&(query.ssl=query.sslmode,delete query.sslmode),"timeout"in o&&(console.log("The timeout option is deprecated, use idle_timeout instead"),o.idle_timeout=o.timeout),query.sslrootcert==="system"&&(query.ssl="verify-full");let ints=["idle_timeout","connect_timeout","max_lifetime","max_pipeline","backoff","keep_alive"],defaults={max:globalThis.Cloudflare?3:10,ssl:!1,sslnegotiation:null,idle_timeout:null,connect_timeout:30,max_lifetime,max_pipeline:100,backoff,keep_alive:60,prepare:!0,debug:!1,fetch_types:!0,publications:"alltables",target_session_attrs:null};return{host:Array.isArray(host)?host:host.split(",").map((x)=>x.split(":")[0]),port:Array.isArray(port)?port:host.split(",").map((x)=>parseInt(x.split(":")[1]||port)),path:o.path||host.indexOf("/")>-1&&host+"/.s.PGSQL."+port,database:o.database||o.db||(url.pathname||"").slice(1)||env.PGDATABASE||user,user,pass:o.pass||o.password||url.password||env.PGPASSWORD||"",...Object.entries(defaults).reduce((acc,[k,d])=>{let value=k in o?o[k]:(k in query)?query[k]==="disable"||query[k]==="false"?!1:query[k]:env["PG"+k.toUpperCase()]||d;return acc[k]=typeof value==="string"&&ints.includes(k)?+value:value,acc},{}),connection:{application_name:env.PGAPPNAME||"postgres.js",...o.connection,...Object.entries(query).reduce((acc,[k,v])=>((k in defaults)||(acc[k]=v),acc),{})},types:o.types||{},target_session_attrs:tsa(o,url,env),onnotice:o.onnotice,onnotify:o.onnotify,onclose:o.onclose,onparameter:o.onparameter,socket:o.socket,transform:parseTransform(o.transform||{undefined:void 0}),parameters:{},shared:{retries:0,typeArrayMap:{}},...mergeUserTypes(o.types)}}function tsa(o,url,env){let x=o.target_session_attrs||url.searchParams.get("target_session_attrs")||env.PGTARGETSESSIONATTRS;if(!x||["read-write","read-only","primary","standby","prefer-standby"].includes(x))return x;throw Error("target_session_attrs "+x+" is not supported")}function backoff(retries){return(0.5+Math.random()/2)*Math.min(3**retries/100,20)}function max_lifetime(){return 60*(30+Math.random()*30)}function parseTransform(x){return{undefined:x.undefined,column:{from:typeof x.column==="function"?x.column:x.column&&x.column.from,to:x.column&&x.column.to},value:{from:typeof x.value==="function"?x.value:x.value&&x.value.from,to:x.value&&x.value.to},row:{from:typeof x.row==="function"?x.row:x.row&&x.row.from,to:x.row&&x.row.to}}}function parseUrl(url){if(!url||typeof url!=="string")return{url:{searchParams:new Map}};let host=url;host=host.slice(host.indexOf("://")+3).split(/[?/]/)[0],host=decodeURIComponent(host.slice(host.indexOf("@")+1));let urlObj=new URL(url.replace(host,host.split(",")[0]));return{url:{username:decodeURIComponent(urlObj.username),password:decodeURIComponent(urlObj.password),host:urlObj.host,hostname:urlObj.hostname,port:urlObj.port,pathname:urlObj.pathname,searchParams:urlObj.searchParams},multihost:host.indexOf(",")>-1&&host}}function osUsername(){try{return os.userInfo().username}catch(_){return process.env.USERNAME||process.env.USER||process.env.LOGNAME}}var src_default;var init_src=__esm(()=>{init_types3();init_connection();init_query();init_queue();init_errors3();init_large();Object.assign(Postgres,{PostgresError,toPascal,pascal,toCamel,camel,toKebab,kebab,fromPascal,fromCamel,fromKebab,BigInt:{to:20,from:[20],parse:(x)=>BigInt(x),serialize:(x)=>x.toString()}});src_default=Postgres});var exports_db={};__export(exports_db,{shutdown:()=>shutdown,isAvailable:()=>isAvailable2,getLockfilePath:()=>getLockfilePath,getDataDir:()=>getDataDir,getConnection:()=>getConnection,getActivePort:()=>getActivePort,ensurePgserve:()=>ensurePgserve});import{execSync as execSync3}from"child_process";import{existsSync as existsSync14,mkdirSync as mkdirSync6,readFileSync as readFileSync8,renameSync,unlinkSync as unlinkSync3,writeFileSync as writeFileSync5}from"fs";import{createConnection}from"net";import{homedir as homedir13}from"os";import{join as join17}from"path";function maskCredentials(url){return url.replace(/\/\/.*@/,"//***@")}function killOrphanedPostgres(dataDir){let pidFile=join17(dataDir,"postmaster.pid");if(!existsSync14(pidFile))return;try{let content=readFileSync8(pidFile,"utf-8"),pid=Number.parseInt(content.split(`
163
- `)[0],10);if(Number.isNaN(pid)||pid<=0)return;let cmdline;try{cmdline=execSync3(`ps -o command= -p ${pid} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return}if(!cmdline.includes("postgres"))return;try{process.kill(pid,"SIGTERM")}catch{return}let deadline=Date.now()+5000;while(Date.now()<deadline)try{process.kill(pid,0),execSync3("sleep 0.2",{stdio:"ignore"})}catch{return}try{process.kill(pid,"SIGKILL")}catch{}}catch{}}function getPort(){let envPort=process.env.GENIE_PG_PORT;if(envPort){let parsed=Number.parseInt(envPort,10);if(!Number.isNaN(parsed)&&parsed>0&&parsed<65536)return parsed}return DEFAULT_PORT}function isPortListening(port,host){return new Promise((resolve4)=>{let socket=createConnection({port,host},()=>{socket.destroy(),resolve4(!0)});socket.on("error",()=>{socket.destroy(),resolve4(!1)}),socket.setTimeout(1000,()=>{socket.destroy(),resolve4(!1)})})}function readLockfile(){try{let content=readFileSync8(LOCKFILE_PATH,"utf-8").trim(),port=Number.parseInt(content,10);if(!Number.isNaN(port)&&port>0&&port<65536)return port}catch{}return null}function writeLockfile(port){try{mkdirSync6(GENIE_HOME2,{recursive:!0});let tmpPath=`${LOCKFILE_PATH}.tmp.${process.pid}`;writeFileSync5(tmpPath,String(port),"utf-8"),renameSync(tmpPath,LOCKFILE_PATH)}catch{}}function removeLockfile(){try{unlinkSync3(LOCKFILE_PATH)}catch{}}async function ensurePgserve(){if(ensurePromise)return ensurePromise;ensurePromise=_ensurePgserve();try{return await ensurePromise}finally{ensurePromise=null}}async function _ensurePgserve(){if(activePort!==null&&pgserveServer)return activePort;if(activePort!==null)return activePort;let port=getPort(),reusedPort=await tryReuseLockfile();if(reusedPort!==null)return reusedPort;if(await isPortListening(port,DEFAULT_HOST))return markPortActive(port,!0);mkdirSync6(DATA_DIR,{recursive:!0}),killOrphanedPostgres(DATA_DIR);try{let startedPort=await startPgserveOnPort(port);return registerExitHandler(),startedPort}catch(err){return tryFallbackPorts(port,err)}}async function tryReuseLockfile(){let lockfilePort=readLockfile();if(lockfilePort===null)return null;if(await isPortListening(lockfilePort,DEFAULT_HOST))return markPortActive(lockfilePort,!1);return removeLockfile(),null}function markPortActive(port,writeLock){if(activePort=port,process.env.GENIE_PG_AVAILABLE="true",writeLock)writeLockfile(port);return port}async function tryFallbackPorts(basePort,originalErr){for(let offset=1;offset<=MAX_PORT_RETRIES;offset++){let fallbackPort=basePort+offset;if(await isPortListening(fallbackPort,DEFAULT_HOST))return markPortActive(fallbackPort,!0);try{let startedPort=await startPgserveOnPort(fallbackPort);return registerExitHandler(),startedPort}catch{}}process.env.GENIE_PG_AVAILABLE="false";let message=originalErr instanceof Error?originalErr.message:String(originalErr);throw console.warn(`Warning: pgserve failed to start: ${maskCredentials(message)}`),Error(`pgserve failed to start on port ${basePort} (and fallbacks ${basePort+1}-${basePort+MAX_PORT_RETRIES}): ${maskCredentials(message)}`)}async function startPgserveOnPort(port){let{startMultiTenantServer}=await import("pgserve");return pgserveServer=await startMultiTenantServer({port,host:DEFAULT_HOST,baseDir:DATA_DIR,logLevel:"warn",autoProvision:!0}),activePort=port,ownsLockfile=!0,process.env.GENIE_PG_AVAILABLE="true",writeLockfile(port),port}function registerExitHandler(){if(exitHandlerRegistered)return;exitHandlerRegistered=!0;let cleanup=()=>{if(ownsLockfile)removeLockfile(),ownsLockfile=!1};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit(130)}),process.on("SIGTERM",()=>{cleanup(),process.exit(143)})}function migrationsDone(){try{let marker=readFileSync8(MIGRATION_MARKER,"utf-8").trim(),currentVersion=process.env.npm_package_version??"";return marker===currentVersion||currentVersion===""&&marker.length>0}catch{return!1}}function markMigrationsDone(){try{let version=process.env.npm_package_version??Date.now().toString();writeFileSync5(MIGRATION_MARKER,version,"utf-8")}catch{}}async function getConnection(){if(sqlClient)return sqlClient;let port=await ensurePgserve(),postgres2=(await Promise.resolve().then(() => (init_src(),exports_src))).default;if(sqlClient=postgres2({host:DEFAULT_HOST,port,database:DB_NAME,username:"postgres",password:"postgres",max:10,idle_timeout:1,connect_timeout:5}),!migrationsDone())await runMigrations(sqlClient),markMigrationsDone();return sqlClient}async function isAvailable2(){try{return await(await getConnection())`SELECT 1`,!0}catch{return!1}}async function shutdown(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null;if(ownsLockfile)removeLockfile(),ownsLockfile=!1}function getDataDir(){return DATA_DIR}function getActivePort(){return activePort??getPort()}function getLockfilePath(){return LOCKFILE_PATH}var DEFAULT_PORT=19642,DEFAULT_HOST="127.0.0.1",MAX_PORT_RETRIES=3,GENIE_HOME2,DATA_DIR,LOCKFILE_PATH,MIGRATION_MARKER,DB_NAME="genie",pgserveServer=null,sqlClient=null,activePort=null,ensurePromise=null,ownsLockfile=!1,exitHandlerRegistered=!1;var init_db=__esm(()=>{init_db_migrations();GENIE_HOME2=process.env.GENIE_HOME??join17(homedir13(),".genie"),DATA_DIR=join17(GENIE_HOME2,"data","pgserve"),LOCKFILE_PATH=join17(GENIE_HOME2,"pgserve.port"),MIGRATION_MARKER=join17(GENIE_HOME2,"pgserve.migrated")});var exports_wish_state={};__export(exports_wish_state,{startGroup:()=>startGroup,resolveRepoPath:()=>resolveRepoPath,resetInProgressGroups:()=>resetInProgressGroups,resetGroup:()=>resetGroup,isWishComplete:()=>isWishComplete,getState:()=>getState,getOrCreateState:()=>getOrCreateState,getGroupState:()=>getGroupState,findGroupByAssignee:()=>findGroupByAssignee,findAnyGroupByAssignee:()=>findAnyGroupByAssignee,createState:()=>createState,completeGroup:()=>completeGroup,WishStateSchema:()=>WishStateSchema,GroupStatusSchema:()=>GroupStatusSchema,GroupStateSchema:()=>GroupStateSchema});import{execSync as execSync4}from"child_process";import{dirname as dirname5}from"path";function resolveRepoPath(cwd){if(cwd)return cwd;try{let commonDir=execSync4("git rev-parse --path-format=absolute --git-common-dir",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();return dirname5(commonDir)}catch{return process.cwd()}}function wishFilePath(slug){return`.genie/wishes/${slug}/WISH.md`}function toISO(v){if(v==null)return;if(v instanceof Date)return v.toISOString();return String(v)}async function findParent(sql,slug,repoPath){let wishFile=wishFilePath(slug),rows=await sql`
162
+ `};return resolve4(lo),new Promise(async(r)=>finish=r);async function readable({highWaterMark=16384,start=0,end=1/0}={}){let max=end-start;return start&&await lo.seek(start),new Stream2.Readable({highWaterMark,async read(size){let l=size>max?size-max:size;max-=size;let[{data}]=await lo.read(l);if(this.push(data),data.length<size)this.push(null)}})}async function writable({highWaterMark=16384,start=0}={}){return start&&await lo.seek(start),new Stream2.Writable({highWaterMark,write(chunk,encoding,callback){lo.write(chunk).then(()=>callback(),callback)}})}}).catch(reject)})}var init_large=()=>{};var exports_src={};__export(exports_src,{default:()=>src_default});import os from"os";import fs from"fs";function Postgres(a,b2){let options=parseOptions(a,b2),subscribe2=options.no_subscribe||Subscribe(Postgres,{...options}),ending=!1,queries=queue_default(),connecting=queue_default(),reserved=queue_default(),closed=queue_default(),ended=queue_default(),open2=queue_default(),busy=queue_default(),full=queue_default(),queues={connecting,reserved,closed,ended,open:open2,busy,full},connections=[...Array(options.max)].map(()=>connection_default(options,queues,{onopen,onend,onclose})),sql=Sql(handler);return Object.assign(sql,{get parameters(){return options.parameters},largeObject:largeObject.bind(null,sql),subscribe:subscribe2,CLOSE,END:CLOSE,PostgresError,options,reserve,listen,begin,close:close2,end}),sql;function Sql(handler2){return handler2.debug=options.debug,Object.entries(options.types).reduce((acc,[name,type2])=>{return acc[name]=(x)=>new Parameter(x,type2.to),acc},typed),Object.assign(sql2,{types:typed,typed,unsafe,notify,array,json:json2,file}),sql2;function typed(value,type2){return new Parameter(value,type2)}function sql2(strings,...args){return strings&&Array.isArray(strings.raw)?new Query(strings,args,handler2,cancel):typeof strings==="string"&&!args.length?new Identifier(options.transform.column.to?options.transform.column.to(strings):strings):new Builder(strings,args)}function unsafe(string,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([string],args,handler2,cancel,{prepare:!1,...options2,simple:"simple"in options2?options2.simple:args.length===0})}function file(path2,args=[],options2={}){return arguments.length===2&&!Array.isArray(args)&&(options2=args,args=[]),new Query([],args,(query2)=>{fs.readFile(path2,"utf8",(err,string)=>{if(err)return query2.reject(err);query2.strings=[string],handler2(query2)})},cancel,{...options2,simple:"simple"in options2?options2.simple:args.length===0})}}async function listen(name,fn,onlisten){let listener={fn,onlisten},sql2=listen.sql||(listen.sql=Postgres({...options,max:1,idle_timeout:null,max_lifetime:null,fetch_types:!1,onclose(){Object.entries(listen.channels).forEach(([name2,{listeners}])=>{delete listen.channels[name2],Promise.all(listeners.map((l)=>listen(name2,l.fn,l.onlisten).catch(()=>{})))})},onnotify(c,x){c in listen.channels&&listen.channels[c].listeners.forEach((l)=>l.fn(x))}})),channels=listen.channels||(listen.channels={});if(name in channels){channels[name].listeners.push(listener);let result2=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result2.state,unlisten}}channels[name]={result:sql2`listen ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`,listeners:[listener]};let result=await channels[name].result;return listener.onlisten&&listener.onlisten(),{state:result.state,unlisten};async function unlisten(){if(name in channels===!1)return;if(channels[name].listeners=channels[name].listeners.filter((x)=>x!==listener),channels[name].listeners.length)return;return delete channels[name],sql2`unlisten ${sql2.unsafe('"'+name.replace(/"/g,'""')+'"')}`}}async function notify(channel,payload){return await sql`select pg_notify(${channel}, ${""+payload})`}async function reserve(){let queue=queue_default(),c=open2.length?open2.shift():await new Promise((resolve4,reject)=>{let query={reserve:resolve4,reject};queries.push(query),closed.length&&connect(closed.shift(),query)});move(c,reserved),c.reserved=()=>queue.length?c.execute(queue.shift()):move(c,reserved),c.reserved.release=!0;let sql2=Sql(handler2);return sql2.release=()=>{c.reserved=null,onopen(c)},sql2;function handler2(q){c.queue===full?queue.push(q):c.execute(q)||move(c,full)}}async function begin(options2,fn){!fn&&(fn=options2,options2="");let queries2=queue_default(),savepoints=0,connection2,prepare=null;try{return await sql.unsafe("begin "+options2.replace(/[^a-z ]/ig,""),[],{onexecute}).execute(),await Promise.race([scope(connection2,fn),new Promise((_,reject)=>connection2.onclose=reject)])}catch(error2){throw error2}async function scope(c,fn2,name){let sql2=Sql(handler2);sql2.savepoint=savepoint,sql2.prepare=(x)=>prepare=x.replace(/[^a-z0-9$-_. ]/gi);let uncaughtError,result;name&&await sql2`savepoint ${sql2(name)}`;try{if(result=await new Promise((resolve4,reject)=>{let x=fn2(sql2);Promise.resolve(Array.isArray(x)?Promise.all(x):x).then(resolve4,reject)}),uncaughtError)throw uncaughtError}catch(e){throw await(name?sql2`rollback to ${sql2(name)}`:sql2`rollback`),e instanceof PostgresError&&e.code==="25P02"&&uncaughtError||e}if(!name)prepare?await sql2`prepare transaction '${sql2.unsafe(prepare)}'`:await sql2`commit`;return result;function savepoint(name2,fn3){if(name2&&Array.isArray(name2.raw))return savepoint((sql3)=>sql3.apply(sql3,arguments));return arguments.length===1&&(fn3=name2,name2=null),scope(c,fn3,"s"+savepoints+++(name2?"_"+name2:""))}function handler2(q){q.catch((e)=>uncaughtError||(uncaughtError=e)),c.queue===full?queries2.push(q):c.execute(q)||move(c,full)}}function onexecute(c){connection2=c,move(c,reserved),c.reserved=()=>queries2.length?c.execute(queries2.shift()):move(c,reserved)}}function move(c,queue){return c.queue.remove(c),queue.push(c),c.queue=queue,queue===open2?c.idleTimer.start():c.idleTimer.cancel(),c}function json2(x){return new Parameter(x,3802)}function array(x,type2){if(!Array.isArray(x))return array(Array.from(arguments));return new Parameter(x,type2||(x.length?inferType(x)||25:0),options.shared.typeArrayMap)}function handler(query){if(ending)return query.reject(Errors.connection("CONNECTION_ENDED",options,options));if(open2.length)return go(open2.shift(),query);if(closed.length)return connect(closed.shift(),query);busy.length?go(busy.shift(),query):queries.push(query)}function go(c,query){return c.execute(query)?move(c,busy):move(c,full)}function cancel(query){return new Promise((resolve4,reject)=>{query.state?query.active?connection_default(options).cancel(query.state,resolve4,reject):query.cancelled={resolve:resolve4,reject}:(queries.remove(query),query.cancelled=!0,query.reject(Errors.generic("57014","canceling statement due to user request")),resolve4())})}async function end({timeout=null}={}){if(ending)return ending;await 1;let timer2;return ending=Promise.race([new Promise((r)=>timeout!==null&&(timer2=setTimeout(destroy,timeout*1000,r))),Promise.all(connections.map((c)=>c.end()).concat(listen.sql?listen.sql.end({timeout:0}):[],subscribe2.sql?subscribe2.sql.end({timeout:0}):[]))]).then(()=>clearTimeout(timer2))}async function close2(){await Promise.all(connections.map((c)=>c.end()))}async function destroy(resolve4){await Promise.all(connections.map((c)=>c.terminate()));while(queries.length)queries.shift().reject(Errors.connection("CONNECTION_DESTROYED",options));resolve4()}function connect(c,query){return move(c,connecting),c.connect(query),c}function onend(c){move(c,ended)}function onopen(c){if(queries.length===0)return move(c,open2);let max=Math.ceil(queries.length/(connecting.length+1)),ready=!0;while(ready&&queries.length&&max-- >0){let query=queries.shift();if(query.reserve)return query.reserve(c);ready=c.execute(query)}ready?move(c,busy):move(c,full)}function onclose(c,e){move(c,closed),c.reserved=null,c.onclose&&(c.onclose(e),c.onclose=null),options.onclose&&options.onclose(c.id),queries.length&&connect(c,queries.shift())}}function parseOptions(a,b2){if(a&&a.shared)return a;let env=process.env,o=(!a||typeof a==="string"?b2:a)||{},{url,multihost}=parseUrl(a),query=[...url.searchParams].reduce((a2,[b3,c])=>(a2[b3]=c,a2),{}),host=o.hostname||o.host||multihost||url.hostname||env.PGHOST||"localhost",port=o.port||url.port||env.PGPORT||5432,user=o.user||o.username||url.username||env.PGUSERNAME||env.PGUSER||osUsername();o.no_prepare&&(o.prepare=!1),query.sslmode&&(query.ssl=query.sslmode,delete query.sslmode),"timeout"in o&&(console.log("The timeout option is deprecated, use idle_timeout instead"),o.idle_timeout=o.timeout),query.sslrootcert==="system"&&(query.ssl="verify-full");let ints=["idle_timeout","connect_timeout","max_lifetime","max_pipeline","backoff","keep_alive"],defaults={max:globalThis.Cloudflare?3:10,ssl:!1,sslnegotiation:null,idle_timeout:null,connect_timeout:30,max_lifetime,max_pipeline:100,backoff,keep_alive:60,prepare:!0,debug:!1,fetch_types:!0,publications:"alltables",target_session_attrs:null};return{host:Array.isArray(host)?host:host.split(",").map((x)=>x.split(":")[0]),port:Array.isArray(port)?port:host.split(",").map((x)=>parseInt(x.split(":")[1]||port)),path:o.path||host.indexOf("/")>-1&&host+"/.s.PGSQL."+port,database:o.database||o.db||(url.pathname||"").slice(1)||env.PGDATABASE||user,user,pass:o.pass||o.password||url.password||env.PGPASSWORD||"",...Object.entries(defaults).reduce((acc,[k,d])=>{let value=k in o?o[k]:(k in query)?query[k]==="disable"||query[k]==="false"?!1:query[k]:env["PG"+k.toUpperCase()]||d;return acc[k]=typeof value==="string"&&ints.includes(k)?+value:value,acc},{}),connection:{application_name:env.PGAPPNAME||"postgres.js",...o.connection,...Object.entries(query).reduce((acc,[k,v])=>((k in defaults)||(acc[k]=v),acc),{})},types:o.types||{},target_session_attrs:tsa(o,url,env),onnotice:o.onnotice,onnotify:o.onnotify,onclose:o.onclose,onparameter:o.onparameter,socket:o.socket,transform:parseTransform(o.transform||{undefined:void 0}),parameters:{},shared:{retries:0,typeArrayMap:{}},...mergeUserTypes(o.types)}}function tsa(o,url,env){let x=o.target_session_attrs||url.searchParams.get("target_session_attrs")||env.PGTARGETSESSIONATTRS;if(!x||["read-write","read-only","primary","standby","prefer-standby"].includes(x))return x;throw Error("target_session_attrs "+x+" is not supported")}function backoff(retries){return(0.5+Math.random()/2)*Math.min(3**retries/100,20)}function max_lifetime(){return 60*(30+Math.random()*30)}function parseTransform(x){return{undefined:x.undefined,column:{from:typeof x.column==="function"?x.column:x.column&&x.column.from,to:x.column&&x.column.to},value:{from:typeof x.value==="function"?x.value:x.value&&x.value.from,to:x.value&&x.value.to},row:{from:typeof x.row==="function"?x.row:x.row&&x.row.from,to:x.row&&x.row.to}}}function parseUrl(url){if(!url||typeof url!=="string")return{url:{searchParams:new Map}};let host=url;host=host.slice(host.indexOf("://")+3).split(/[?/]/)[0],host=decodeURIComponent(host.slice(host.indexOf("@")+1));let urlObj=new URL(url.replace(host,host.split(",")[0]));return{url:{username:decodeURIComponent(urlObj.username),password:decodeURIComponent(urlObj.password),host:urlObj.host,hostname:urlObj.hostname,port:urlObj.port,pathname:urlObj.pathname,searchParams:urlObj.searchParams},multihost:host.indexOf(",")>-1&&host}}function osUsername(){try{return os.userInfo().username}catch(_){return process.env.USERNAME||process.env.USER||process.env.LOGNAME}}var src_default;var init_src=__esm(()=>{init_types3();init_connection();init_query();init_queue();init_errors3();init_large();Object.assign(Postgres,{PostgresError,toPascal,pascal,toCamel,camel,toKebab,kebab,fromPascal,fromCamel,fromKebab,BigInt:{to:20,from:[20],parse:(x)=>BigInt(x),serialize:(x)=>x.toString()}});src_default=Postgres});var exports_db={};__export(exports_db,{shutdown:()=>shutdown,resetConnection:()=>resetConnection,isAvailable:()=>isAvailable2,getLockfilePath:()=>getLockfilePath,getDataDir:()=>getDataDir,getConnection:()=>getConnection,getActivePort:()=>getActivePort,ensurePgserve:()=>ensurePgserve});import{execSync as execSync3}from"child_process";import{existsSync as existsSync14,mkdirSync as mkdirSync6,readFileSync as readFileSync8,renameSync,unlinkSync as unlinkSync3,writeFileSync as writeFileSync5}from"fs";import{createConnection}from"net";import{homedir as homedir13}from"os";import{join as join17}from"path";function maskCredentials(url){return url.replace(/\/\/.*@/,"//***@")}function killOrphanedPostgres(dataDir){let pidFile=join17(dataDir,"postmaster.pid");if(!existsSync14(pidFile))return;try{let content=readFileSync8(pidFile,"utf-8"),pid=Number.parseInt(content.split(`
163
+ `)[0],10);if(Number.isNaN(pid)||pid<=0)return;let cmdline;try{cmdline=execSync3(`ps -o command= -p ${pid} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return}if(!cmdline.includes("postgres"))return;try{process.kill(pid,"SIGTERM")}catch{return}let deadline=Date.now()+5000;while(Date.now()<deadline)try{process.kill(pid,0),execSync3("sleep 0.2",{stdio:"ignore"})}catch{return}try{process.kill(pid,"SIGKILL")}catch{}}catch{}}function getPort(){let envPort=process.env.GENIE_PG_PORT;if(envPort){let parsed=Number.parseInt(envPort,10);if(!Number.isNaN(parsed)&&parsed>0&&parsed<65536)return parsed}return DEFAULT_PORT}function isPortListening(port,host){return new Promise((resolve4)=>{let socket=createConnection({port,host},()=>{socket.destroy(),resolve4(!0)});socket.on("error",()=>{socket.destroy(),resolve4(!1)}),socket.setTimeout(1000,()=>{socket.destroy(),resolve4(!1)})})}function readLockfile(){try{let content=readFileSync8(LOCKFILE_PATH,"utf-8").trim(),port=Number.parseInt(content,10);if(!Number.isNaN(port)&&port>0&&port<65536)return port}catch{}return null}function writeLockfile(port){try{mkdirSync6(GENIE_HOME2,{recursive:!0});let tmpPath=`${LOCKFILE_PATH}.tmp.${process.pid}`;writeFileSync5(tmpPath,String(port),"utf-8"),renameSync(tmpPath,LOCKFILE_PATH)}catch{}}function removeLockfile(){try{unlinkSync3(LOCKFILE_PATH)}catch{}}async function ensurePgserve(){if(ensurePromise)return ensurePromise;ensurePromise=_ensurePgserve();try{return await ensurePromise}finally{ensurePromise=null}}async function _ensurePgserve(){if(activePort!==null&&pgserveServer)return activePort;if(activePort!==null)return activePort;let port=getPort(),reusedPort=await tryReuseLockfile();if(reusedPort!==null)return reusedPort;if(await isPortListening(port,DEFAULT_HOST))return markPortActive(port,!0);mkdirSync6(DATA_DIR,{recursive:!0}),killOrphanedPostgres(DATA_DIR);try{let startedPort=await startPgserveOnPort(port);return registerExitHandler(),startedPort}catch(err){return tryFallbackPorts(port,err)}}async function tryReuseLockfile(){let lockfilePort=readLockfile();if(lockfilePort===null)return null;if(await isPortListening(lockfilePort,DEFAULT_HOST))return markPortActive(lockfilePort,!1);return removeLockfile(),null}function markPortActive(port,writeLock){if(activePort=port,process.env.GENIE_PG_AVAILABLE="true",writeLock)writeLockfile(port);return port}async function tryFallbackPorts(basePort,originalErr){for(let offset=1;offset<=MAX_PORT_RETRIES;offset++){let fallbackPort=basePort+offset;if(await isPortListening(fallbackPort,DEFAULT_HOST))return markPortActive(fallbackPort,!0);try{let startedPort=await startPgserveOnPort(fallbackPort);return registerExitHandler(),startedPort}catch{}}process.env.GENIE_PG_AVAILABLE="false";let message=originalErr instanceof Error?originalErr.message:String(originalErr);throw console.warn(`Warning: pgserve failed to start: ${maskCredentials(message)}`),Error(`pgserve failed to start on port ${basePort} (and fallbacks ${basePort+1}-${basePort+MAX_PORT_RETRIES}): ${maskCredentials(message)}`)}async function startPgserveOnPort(port){let{startMultiTenantServer}=await import("pgserve");return pgserveServer=await startMultiTenantServer({port,host:DEFAULT_HOST,baseDir:DATA_DIR,logLevel:"warn",autoProvision:!0}),activePort=port,ownsLockfile=!0,process.env.GENIE_PG_AVAILABLE="true",writeLockfile(port),port}function registerExitHandler(){if(exitHandlerRegistered)return;exitHandlerRegistered=!0;let cleanup=()=>{if(ownsLockfile)removeLockfile(),ownsLockfile=!1};process.on("exit",cleanup),process.on("SIGINT",()=>{cleanup(),process.exit(130)}),process.on("SIGTERM",()=>{cleanup(),process.exit(143)})}function migrationsDone(){try{let marker=readFileSync8(MIGRATION_MARKER,"utf-8").trim(),currentVersion=process.env.npm_package_version??"";return marker===currentVersion||currentVersion===""&&marker.length>0}catch{return!1}}function markMigrationsDone(){try{let version=process.env.npm_package_version??Date.now().toString();writeFileSync5(MIGRATION_MARKER,version,"utf-8")}catch{}}async function getConnection(){if(sqlClient)return sqlClient;let port=await ensurePgserve(),postgres2=(await Promise.resolve().then(() => (init_src(),exports_src))).default,testSchema=process.env.GENIE_TEST_SCHEMA;if(sqlClient=postgres2({host:DEFAULT_HOST,port,database:DB_NAME,username:"postgres",password:"postgres",max:10,idle_timeout:1,connect_timeout:5,...testSchema?{connection:{search_path:`${testSchema}, public`}}:{}}),!migrationsDone())await runMigrations(sqlClient),markMigrationsDone();return sqlClient}async function resetConnection(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null}async function isAvailable2(){try{return await(await getConnection())`SELECT 1`,!0}catch{return!1}}async function shutdown(){if(sqlClient)await sqlClient.end({timeout:5}),sqlClient=null;if(ownsLockfile)removeLockfile(),ownsLockfile=!1}function getDataDir(){return DATA_DIR}function getActivePort(){return activePort??getPort()}function getLockfilePath(){return LOCKFILE_PATH}var DEFAULT_PORT=19642,DEFAULT_HOST="127.0.0.1",MAX_PORT_RETRIES=3,GENIE_HOME2,DATA_DIR,LOCKFILE_PATH,MIGRATION_MARKER,DB_NAME="genie",pgserveServer=null,sqlClient=null,activePort=null,ensurePromise=null,ownsLockfile=!1,exitHandlerRegistered=!1;var init_db=__esm(()=>{init_db_migrations();GENIE_HOME2=process.env.GENIE_HOME??join17(homedir13(),".genie"),DATA_DIR=join17(GENIE_HOME2,"data","pgserve"),LOCKFILE_PATH=join17(GENIE_HOME2,"pgserve.port"),MIGRATION_MARKER=join17(GENIE_HOME2,"pgserve.migrated")});var exports_wish_state={};__export(exports_wish_state,{startGroup:()=>startGroup,resolveRepoPath:()=>resolveRepoPath,resetInProgressGroups:()=>resetInProgressGroups,resetGroup:()=>resetGroup,isWishComplete:()=>isWishComplete,getState:()=>getState,getOrCreateState:()=>getOrCreateState,getGroupState:()=>getGroupState,findGroupByAssignee:()=>findGroupByAssignee,findAnyGroupByAssignee:()=>findAnyGroupByAssignee,createState:()=>createState,completeGroup:()=>completeGroup,WishStateSchema:()=>WishStateSchema,GroupStatusSchema:()=>GroupStatusSchema,GroupStateSchema:()=>GroupStateSchema});import{execSync as execSync4}from"child_process";import{dirname as dirname5}from"path";function resolveRepoPath(cwd){if(cwd)return cwd;try{let commonDir=execSync4("git rev-parse --path-format=absolute --git-common-dir",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim();return dirname5(commonDir)}catch{return process.cwd()}}function wishFilePath(slug){return`.genie/wishes/${slug}/WISH.md`}function toISO(v){if(v==null)return;if(v instanceof Date)return v.toISOString();return String(v)}async function findParent(sql,slug,repoPath){let wishFile=wishFilePath(slug),rows=await sql`
164
164
  SELECT * FROM tasks
165
165
  WHERE wish_file = ${wishFile} AND repo_path = ${repoPath} AND parent_id IS NULL
166
166
  LIMIT 1
@@ -2,7 +2,7 @@
2
2
  "id": "genie",
3
3
  "name": "Genie",
4
4
  "description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
5
- "version": "4.260325.4",
5
+ "version": "4.260325.6",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260325.4",
3
+ "version": "4.260325.6",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260325.4",
3
+ "version": "4.260325.6",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260325.4",
3
+ "version": "4.260325.6",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
package/src/lib/db.ts CHANGED
@@ -318,6 +318,9 @@ function markMigrationsDone(): void {
318
318
  /**
319
319
  * Get a postgres.js connection. Lazy singleton — calls ensurePgserve() on first use.
320
320
  * Returns a postgres.js sql tagged template client.
321
+ *
322
+ * When GENIE_TEST_SCHEMA is set, all connections use that schema in their search_path.
323
+ * This isolates test data from production tables.
321
324
  */
322
325
  export async function getConnection() {
323
326
  if (sqlClient) return sqlClient;
@@ -325,6 +328,7 @@ export async function getConnection() {
325
328
  const port = await ensurePgserve();
326
329
  const postgres = (await import('postgres')).default;
327
330
 
331
+ const testSchema = process.env.GENIE_TEST_SCHEMA;
328
332
  sqlClient = postgres({
329
333
  host: DEFAULT_HOST,
330
334
  port,
@@ -334,6 +338,7 @@ export async function getConnection() {
334
338
  max: 10,
335
339
  idle_timeout: 1,
336
340
  connect_timeout: 5,
341
+ ...(testSchema ? { connection: { search_path: `${testSchema}, public` } } : {}),
337
342
  });
338
343
 
339
344
  // Only run migrations if not yet applied for this version
@@ -345,6 +350,17 @@ export async function getConnection() {
345
350
  return sqlClient;
346
351
  }
347
352
 
353
+ /**
354
+ * Reset the connection singleton. Next call to getConnection() creates a fresh client.
355
+ * Used by test helpers to switch schemas between test runs.
356
+ */
357
+ export async function resetConnection(): Promise<void> {
358
+ if (sqlClient) {
359
+ await sqlClient.end({ timeout: 5 });
360
+ sqlClient = null;
361
+ }
362
+ }
363
+
348
364
  /**
349
365
  * Non-throwing health check. Returns true if pgserve is reachable and responds to queries.
350
366
  */
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
9
- import { getConnection, shutdown } from './db.js';
9
+ import { getConnection } from './db.js';
10
10
  import {
11
11
  type Actor,
12
12
  addDependency,
@@ -63,6 +63,7 @@ import {
63
63
  updateMessage,
64
64
  updateTask,
65
65
  } from './task-service.js';
66
+ import { setupTestSchema } from './test-db.js';
66
67
 
67
68
  // Unique repo path per test run to avoid collisions
68
69
  const REPO = `/tmp/test-repo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -70,18 +71,15 @@ const actor: Actor = { actorType: 'local', actorId: 'test-user' };
70
71
  const actor2: Actor = { actorType: 'local', actorId: 'test-user-2' };
71
72
 
72
73
  let sql: Awaited<ReturnType<typeof getConnection>>;
74
+ let cleanupSchema: () => Promise<void>;
73
75
 
74
76
  beforeAll(async () => {
77
+ cleanupSchema = await setupTestSchema();
75
78
  sql = await getConnection();
76
79
  });
77
80
 
78
81
  afterAll(async () => {
79
- // Clean up test data
80
- if (sql) {
81
- await sql`DELETE FROM tasks WHERE repo_path = ${REPO}`;
82
- await sql`DELETE FROM tasks WHERE repo_path LIKE '/tmp/test-repo-%'`;
83
- }
84
- await shutdown();
82
+ await cleanupSchema();
85
83
  });
86
84
 
87
85
  // ============================================================================
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Test Database Helpers — PG schema isolation for tests.
3
+ *
4
+ * Each test file gets its own PG schema so test data never touches
5
+ * the production `public` schema. Schemas are created in beforeAll
6
+ * and dropped in afterAll — zero artifacts after `bun test`.
7
+ *
8
+ * Usage:
9
+ * import { setupTestSchema, teardownTestSchema } from './test-db.js';
10
+ *
11
+ * let cleanup: () => Promise<void>;
12
+ * beforeAll(async () => { cleanup = await setupTestSchema(); });
13
+ * afterAll(async () => { await cleanup(); });
14
+ */
15
+
16
+ import { runMigrations } from './db-migrations.js';
17
+ import { ensurePgserve, resetConnection } from './db.js';
18
+
19
+ /**
20
+ * Create an isolated PG schema for this test file.
21
+ * Returns a cleanup function that drops the schema and resets the connection.
22
+ *
23
+ * How it works:
24
+ * 1. Gets a default connection (no schema override)
25
+ * 2. Creates a unique schema named `test_<pid>_<timestamp>`
26
+ * 3. Runs all migrations inside that schema
27
+ * 4. Resets the connection singleton
28
+ * 5. Sets GENIE_TEST_SCHEMA env var so getConnection() uses the test schema
29
+ *
30
+ * All subsequent getConnection() calls in this process will use the test schema.
31
+ */
32
+ export async function setupTestSchema(): Promise<() => Promise<void>> {
33
+ const schemaName = `test_${process.pid}_${Date.now()}`;
34
+
35
+ // Get a raw connection to create the schema
36
+ const port = await ensurePgserve();
37
+ const postgres = (await import('postgres')).default;
38
+ const adminSql = postgres({
39
+ host: '127.0.0.1',
40
+ port,
41
+ database: 'genie',
42
+ username: 'postgres',
43
+ password: 'postgres',
44
+ max: 2,
45
+ idle_timeout: 1,
46
+ connect_timeout: 5,
47
+ });
48
+
49
+ // Create test schema and run migrations inside it
50
+ await adminSql.unsafe(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
51
+ await adminSql.unsafe(`SET search_path TO "${schemaName}", public`);
52
+ await runMigrations(adminSql);
53
+ await adminSql.end({ timeout: 5 });
54
+
55
+ // Reset the singleton and set the env var so getConnection() picks up the schema
56
+ await resetConnection();
57
+ process.env.GENIE_TEST_SCHEMA = schemaName;
58
+
59
+ // Return cleanup function
60
+ return async () => {
61
+ // Reset connection before dropping schema
62
+ await resetConnection();
63
+
64
+ // Drop the test schema with a fresh admin connection
65
+ const cleanupSql = postgres({
66
+ host: '127.0.0.1',
67
+ port,
68
+ database: 'genie',
69
+ username: 'postgres',
70
+ password: 'postgres',
71
+ max: 2,
72
+ idle_timeout: 1,
73
+ connect_timeout: 5,
74
+ });
75
+
76
+ try {
77
+ await cleanupSql.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
78
+ } finally {
79
+ await cleanupSql.end({ timeout: 5 });
80
+ }
81
+
82
+ // Clear env var
83
+ process.env.GENIE_TEST_SCHEMA = undefined;
84
+ };
85
+ }
@@ -10,7 +10,7 @@ import { execSync } from 'node:child_process';
10
10
  import { mkdtempSync, rmSync } from 'node:fs';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { join } from 'node:path';
13
- import { getConnection, shutdown } from './db.js';
13
+ import { setupTestSchema } from './test-db.js';
14
14
  import {
15
15
  type GroupDefinition,
16
16
  completeGroup,
@@ -28,13 +28,14 @@ import {
28
28
  } from './wish-state.js';
29
29
 
30
30
  let cwd: string;
31
+ let cleanupSchema: () => Promise<void>;
31
32
 
32
33
  beforeAll(async () => {
33
- await getConnection();
34
+ cleanupSchema = await setupTestSchema();
34
35
  });
35
36
 
36
37
  afterAll(async () => {
37
- await shutdown();
38
+ await cleanupSchema();
38
39
  });
39
40
 
40
41
  beforeEach(() => {
@@ -7,18 +7,19 @@
7
7
 
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
9
9
  import type { Agent } from '../lib/agent-registry.js';
10
- import { getConnection, shutdown } from '../lib/db.js';
10
+ import { setupTestSchema } from '../lib/test-db.js';
11
11
  import * as wishState from '../lib/wish-state.js';
12
12
  import { buildResumeContext } from './agents.js';
13
13
 
14
14
  let cwd: string;
15
+ let cleanupSchema: () => Promise<void>;
15
16
 
16
17
  beforeAll(async () => {
17
- await getConnection();
18
+ cleanupSchema = await setupTestSchema();
18
19
  });
19
20
 
20
21
  afterAll(async () => {
21
- await shutdown();
22
+ await cleanupSchema();
22
23
  });
23
24
 
24
25
  beforeEach(() => {
@@ -12,12 +12,11 @@
12
12
  * 8. reviewCommand() — review with diff context
13
13
  */
14
14
 
15
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
15
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
16
16
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
17
17
  import { join } from 'node:path';
18
+ import { setupTestSchema } from '../lib/test-db.js';
18
19
  import * as wishState from '../lib/wish-state.js';
19
- import { parseRef } from './state.js';
20
-
21
20
  import {
22
21
  buildContextPrompt,
23
22
  detectWorkMode,
@@ -27,6 +26,17 @@ import {
27
26
  parseWishGroups,
28
27
  writeContextFile,
29
28
  } from './dispatch.js';
29
+ import { parseRef } from './state.js';
30
+
31
+ let cleanupSchema: () => Promise<void>;
32
+
33
+ beforeAll(async () => {
34
+ cleanupSchema = await setupTestSchema();
35
+ });
36
+
37
+ afterAll(async () => {
38
+ await cleanupSchema();
39
+ });
30
40
 
31
41
  // ============================================================================
32
42
  // Sample WISH.md content for testing
@@ -2,13 +2,24 @@
2
2
  * Tests for state commands — wave detection, push enforcement, pane auto-kill.
3
3
  */
4
4
 
5
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
5
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
6
6
  import { execSync } from 'node:child_process';
7
7
  import { mkdir, rm, writeFile } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
+ import { setupTestSchema } from '../lib/test-db.js';
9
10
  import * as wishState from '../lib/wish-state.js';
10
11
  import { detectWaveCompletion, ensureWorkPushed, parseRef, resolveWishPath } from './state.js';
11
12
 
13
+ let cleanupSchema: () => Promise<void>;
14
+
15
+ beforeAll(async () => {
16
+ cleanupSchema = await setupTestSchema();
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await cleanupSchema();
21
+ });
22
+
12
23
  // ============================================================================
13
24
  // Sample WISH.md with Execution Strategy for wave detection tests
14
25
  // ============================================================================