@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.genie/agents/metrics-updater/AGENT.md +23 -11
- package/.genie/agents/metrics-updater/runs.jsonl +1 -0
- package/.genie/agents/metrics-updater/tools/commit-formatter.sh +5 -12
- package/.genie/agents/metrics-updater/tools/parse-metrics.py +68 -5
- package/.genie/agents/metrics-updater/tools/run-metrics.sh +3 -0
- package/.genie/agents/metrics-updater/tools/update-readme.py +26 -18
- package/dist/genie.js +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/lib/db.ts +16 -0
- package/src/lib/task-service.test.ts +5 -7
- package/src/lib/test-db.ts +85 -0
- package/src/lib/wish-state.test.ts +4 -3
- package/src/term-commands/agents.test.ts +4 -3
- package/src/term-commands/dispatch.test.ts +13 -3
- package/src/term-commands/state.test.ts +12 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260325.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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> [--
|
|
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
|
|
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':
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
'',
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
f'|
|
|
35
|
-
f'|
|
|
36
|
-
f'|
|
|
37
|
-
f'|
|
|
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
|
|
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
|
-
|
|
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
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260325.
|
|
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"
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
34
|
+
cleanupSchema = await setupTestSchema();
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
afterAll(async () => {
|
|
37
|
-
await
|
|
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 {
|
|
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
|
|
18
|
+
cleanupSchema = await setupTestSchema();
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
afterAll(async () => {
|
|
21
|
-
await
|
|
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
|
// ============================================================================
|