panopticon-cli 0.5.1 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-5OPQKM5K.js → agents-HNMF52RM.js} +7 -6
- package/dist/{chunk-F5555J3A.js → chunk-4HST45MO.js} +13 -27
- package/dist/chunk-4HST45MO.js.map +1 -0
- package/dist/{chunk-YLPSQAM2.js → chunk-565HZ6VV.js} +2 -2
- package/dist/chunk-6N2KBSJA.js +452 -0
- package/dist/chunk-6N2KBSJA.js.map +1 -0
- package/dist/{chunk-4YSYJ4HM.js → chunk-DFNVHK3N.js} +2 -2
- package/dist/{chunk-7SN4L4PH.js → chunk-HOGYHJ2G.js} +2 -2
- package/dist/{chunk-FTCPTHIJ.js → chunk-HRU7S4TA.js} +24 -7
- package/dist/chunk-HRU7S4TA.js.map +1 -0
- package/dist/{chunk-OWHXCGVO.js → chunk-ID4OYXVH.js} +378 -101
- package/dist/chunk-ID4OYXVH.js.map +1 -0
- package/dist/{chunk-NLQRED36.js → chunk-KBHRXV5T.js} +3 -3
- package/dist/chunk-KBHRXV5T.js.map +1 -0
- package/dist/{chunk-VHKSS7QX.js → chunk-KY2E2Q3T.js} +25 -19
- package/dist/chunk-KY2E2Q3T.js.map +1 -0
- package/dist/{chunk-CWELWPWQ.js → chunk-MOPGR3CL.js} +1 -1
- package/dist/chunk-MOPGR3CL.js.map +1 -0
- package/dist/{chunk-2V4NF7J2.js → chunk-RLZQB7HS.js} +2 -2
- package/dist/chunk-RLZQB7HS.js.map +1 -0
- package/dist/{chunk-76F6DSVS.js → chunk-T7BBPDEJ.js} +11 -7
- package/dist/chunk-T7BBPDEJ.js.map +1 -0
- package/dist/chunk-USYP2SBE.js +317 -0
- package/dist/chunk-USYP2SBE.js.map +1 -0
- package/dist/{chunk-JM6V62LT.js → chunk-ZDNQFWR5.js} +2 -2
- package/dist/{chunk-JM6V62LT.js.map → chunk-ZDNQFWR5.js.map} +1 -1
- package/dist/{chunk-HJSM6E6U.js → chunk-ZP6EWSZV.js} +29 -322
- package/dist/chunk-ZP6EWSZV.js.map +1 -0
- package/dist/{chunk-PELXV435.js → chunk-ZTYHZMEC.js} +2 -2
- package/dist/chunk-ZTYHZMEC.js.map +1 -0
- package/dist/cli/index.js +1390 -777
- package/dist/cli/index.js.map +1 -1
- package/dist/config-yaml-OVZLKFMA.js +18 -0
- package/dist/dashboard/prompts/merge-agent.md +7 -5
- package/dist/dashboard/prompts/review-agent.md +12 -1
- package/dist/dashboard/prompts/test-agent.md +3 -1
- package/dist/dashboard/public/assets/index-DA6pnizT.js +767 -0
- package/dist/dashboard/public/assets/index-DSvt5pPn.css +32 -0
- package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/dashboard/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-BflQw4A9.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-DxxdqCpr.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-VcznFIpX.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-D6zpsUhD.woff +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-DUi7WF5p.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/dashboard/public/index.html +5 -3
- package/dist/dashboard/server.js +4728 -2767
- package/dist/{feedback-writer-VRMMWWTW.js → feedback-writer-T43PI5S2.js} +2 -2
- package/dist/{hume-WMAUBBV2.js → hume-CKJJ3OUU.js} +3 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/{projects-CFX3RTDL.js → projects-KVM3MN3Y.js} +2 -2
- package/dist/{remote-agents-TFSMW7GN.js → remote-agents-ULPD6C5U.js} +3 -3
- package/dist/{remote-workspace-7FPGF2RM.js → remote-workspace-XX6ARE6I.js} +3 -3
- package/dist/{review-status-TDPSOU5J.js → review-status-XKUKZF6J.js} +3 -2
- package/dist/{specialist-context-WGUUYDWY.js → specialist-context-C66TEMXS.js} +6 -5
- package/dist/{specialist-context-WGUUYDWY.js.map → specialist-context-C66TEMXS.js.map} +1 -1
- package/dist/{specialist-logs-XJB5TCKJ.js → specialist-logs-CJKXM3SR.js} +6 -5
- package/dist/{specialists-5LBRHYFA.js → specialists-NXYD4Z62.js} +6 -5
- package/dist/{traefik-WFMQX2LY.js → traefik-5GL3Q7DJ.js} +3 -3
- package/dist/{tunnel-W2GZBLEV.js → tunnel-BKC7KLBX.js} +3 -3
- package/dist/{workspace-manager-E434Z45T.js → workspace-manager-ALBR62AS.js} +5 -5
- package/dist/workspace-manager-ALBR62AS.js.map +1 -0
- package/package.json +1 -1
- package/scripts/record-cost-event.js +8424 -42
- package/scripts/recover-costs-deep.mjs +209 -0
- package/scripts/recover-costs-proportional.mjs +206 -0
- package/scripts/recover-costs.mjs +169 -0
- package/scripts/work-agent-stop-hook +221 -24
- package/dist/chunk-2V4NF7J2.js.map +0 -1
- package/dist/chunk-76F6DSVS.js.map +0 -1
- package/dist/chunk-CWELWPWQ.js.map +0 -1
- package/dist/chunk-F5555J3A.js.map +0 -1
- package/dist/chunk-FTCPTHIJ.js.map +0 -1
- package/dist/chunk-HJSM6E6U.js.map +0 -1
- package/dist/chunk-NLQRED36.js.map +0 -1
- package/dist/chunk-OWHXCGVO.js.map +0 -1
- package/dist/chunk-PELXV435.js.map +0 -1
- package/dist/chunk-VHKSS7QX.js.map +0 -1
- package/dist/chunk-YGJ54GW2.js +0 -96
- package/dist/chunk-YGJ54GW2.js.map +0 -1
- package/dist/dashboard/public/assets/index-Ce6q21Fm.js +0 -743
- package/dist/dashboard/public/assets/index-NzpI0ItZ.css +0 -32
- package/dist/git-utils-I2UDKNZH.js +0 -131
- package/dist/git-utils-I2UDKNZH.js.map +0 -1
- /package/dist/{agents-5OPQKM5K.js.map → agents-HNMF52RM.js.map} +0 -0
- /package/dist/{chunk-YLPSQAM2.js.map → chunk-565HZ6VV.js.map} +0 -0
- /package/dist/{chunk-4YSYJ4HM.js.map → chunk-DFNVHK3N.js.map} +0 -0
- /package/dist/{chunk-7SN4L4PH.js.map → chunk-HOGYHJ2G.js.map} +0 -0
- /package/dist/{hume-WMAUBBV2.js.map → config-yaml-OVZLKFMA.js.map} +0 -0
- /package/dist/{feedback-writer-VRMMWWTW.js.map → feedback-writer-T43PI5S2.js.map} +0 -0
- /package/dist/{projects-CFX3RTDL.js.map → hume-CKJJ3OUU.js.map} +0 -0
- /package/dist/{remote-agents-TFSMW7GN.js.map → projects-KVM3MN3Y.js.map} +0 -0
- /package/dist/{review-status-TDPSOU5J.js.map → remote-agents-ULPD6C5U.js.map} +0 -0
- /package/dist/{remote-workspace-7FPGF2RM.js.map → remote-workspace-XX6ARE6I.js.map} +0 -0
- /package/dist/{specialist-logs-XJB5TCKJ.js.map → review-status-XKUKZF6J.js.map} +0 -0
- /package/dist/{specialists-5LBRHYFA.js.map → specialist-logs-CJKXM3SR.js.map} +0 -0
- /package/dist/{traefik-WFMQX2LY.js.map → specialists-NXYD4Z62.js.map} +0 -0
- /package/dist/{tunnel-W2GZBLEV.js.map → traefik-5GL3Q7DJ.js.map} +0 -0
- /package/dist/{workspace-manager-E434Z45T.js.map → tunnel-BKC7KLBX.js.map} +0 -0
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
# ~/.panopticon/bin/work-agent-stop-hook
|
|
3
3
|
# Called when any agent goes idle — detects work agents that forgot "pan work done"
|
|
4
4
|
#
|
|
5
|
-
# Uses
|
|
6
|
-
#
|
|
7
|
-
# If so, sends a nudge message to the agent via tmux.
|
|
5
|
+
# Uses evidence-based detection first (STATE.md, git, beads), then falls back to
|
|
6
|
+
# a lightweight AI model for ambiguous cases. Writes resolution to runtime.json.
|
|
8
7
|
#
|
|
9
8
|
# The model used is configurable via PANOPTICON_COMPLETION_CHECK_MODEL env var
|
|
10
9
|
# or defaults to the models.overrides.completion-check-hook setting in config.yaml,
|
|
@@ -37,14 +36,18 @@ esac
|
|
|
37
36
|
# Extract issue ID from agent ID (e.g., "agent-min-725" -> "MIN-725")
|
|
38
37
|
ISSUE_ID=$(echo "$AGENT_ID" | sed 's/^agent-//' | tr '[:lower:]' '[:upper:]')
|
|
39
38
|
|
|
39
|
+
AGENT_STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
|
|
40
|
+
LOG_DIR="$HOME/.panopticon/logs"
|
|
41
|
+
mkdir -p "$LOG_DIR"
|
|
42
|
+
|
|
40
43
|
# Skip if completion marker already exists (agent already called pan work done)
|
|
41
|
-
COMPLETED_FILE="$
|
|
44
|
+
COMPLETED_FILE="$AGENT_STATE_DIR/completed"
|
|
42
45
|
if [ -f "$COMPLETED_FILE" ]; then
|
|
43
46
|
exit 0
|
|
44
47
|
fi
|
|
45
48
|
|
|
46
49
|
# Cooldown: don't nudge more than once per 10 minutes
|
|
47
|
-
NUDGE_FILE="$
|
|
50
|
+
NUDGE_FILE="$AGENT_STATE_DIR/.last-completion-nudge"
|
|
48
51
|
if [ -f "$NUDGE_FILE" ]; then
|
|
49
52
|
LAST_NUDGE=$(cat "$NUDGE_FILE" 2>/dev/null || echo "0")
|
|
50
53
|
NOW=$(date +%s)
|
|
@@ -54,15 +57,151 @@ if [ -f "$NUDGE_FILE" ]; then
|
|
|
54
57
|
fi
|
|
55
58
|
fi
|
|
56
59
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# Helper: write resolution to runtime.json
|
|
62
|
+
# ============================================================================
|
|
63
|
+
write_resolution() {
|
|
64
|
+
local resolution="$1"
|
|
65
|
+
local runtime_file="$AGENT_STATE_DIR/runtime.json"
|
|
66
|
+
|
|
67
|
+
# Read existing resolution count
|
|
68
|
+
local existing_count=0
|
|
69
|
+
local existing_resolution=""
|
|
70
|
+
if [ -f "$runtime_file" ]; then
|
|
71
|
+
existing_resolution=$(python3 -c "import json,sys; d=json.load(open('$runtime_file')); print(d.get('resolution',''))" 2>/dev/null || echo "")
|
|
72
|
+
existing_count=$(python3 -c "import json,sys; d=json.load(open('$runtime_file')); print(d.get('resolutionCount',0))" 2>/dev/null || echo "0")
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Increment count if same resolution, reset if changed
|
|
76
|
+
local new_count
|
|
77
|
+
if [ "$existing_resolution" = "$resolution" ]; then
|
|
78
|
+
new_count=$(( existing_count + 1 ))
|
|
79
|
+
else
|
|
80
|
+
new_count=1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
local now_iso
|
|
84
|
+
now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
85
|
+
|
|
86
|
+
# Merge into runtime.json using python3 (jq not always available)
|
|
87
|
+
python3 - <<PYEOF 2>/dev/null || true
|
|
88
|
+
import json, os
|
|
89
|
+
|
|
90
|
+
runtime_file = "$runtime_file"
|
|
91
|
+
resolution = "$resolution"
|
|
92
|
+
new_count = $new_count
|
|
93
|
+
now_iso = "$now_iso"
|
|
94
|
+
|
|
95
|
+
# Read existing
|
|
96
|
+
data = {}
|
|
97
|
+
if os.path.exists(runtime_file):
|
|
98
|
+
try:
|
|
99
|
+
with open(runtime_file) as f:
|
|
100
|
+
data = json.load(f)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# Update resolution fields
|
|
105
|
+
data['resolution'] = resolution
|
|
106
|
+
data['resolutionCount'] = new_count
|
|
107
|
+
data['resolutionUpdatedAt'] = now_iso
|
|
108
|
+
|
|
109
|
+
# Write back
|
|
110
|
+
with open(runtime_file, 'w') as f:
|
|
111
|
+
json.dump(data, f, indent=2)
|
|
112
|
+
PYEOF
|
|
113
|
+
|
|
114
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID resolution=$resolution count=$new_count" \
|
|
115
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# Phase 1: Evidence-Based Completion Detection
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
# Read workspace path from state.json
|
|
123
|
+
WORKSPACE=""
|
|
124
|
+
STATE_FILE="$AGENT_STATE_DIR/state.json"
|
|
125
|
+
if [ -f "$STATE_FILE" ]; then
|
|
126
|
+
WORKSPACE=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('workspace',''))" 2>/dev/null || echo "")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
STATE_MD_COMPLETE=false
|
|
130
|
+
BRANCH_PUSHED=false
|
|
131
|
+
BEADS_CLOSED=false
|
|
132
|
+
|
|
133
|
+
# Check 1: STATE.md completion markers
|
|
134
|
+
if [ -n "$WORKSPACE" ] && [ -f "$WORKSPACE/.planning/STATE.md" ]; then
|
|
135
|
+
if grep -qiE '(^##?\s*(Status|Current Status).*:.*\b(COMPLETE|DONE|FINISHED)\b|^Status:\s*(COMPLETE|DONE|FINISHED)|implementation[[:space:]]+complete|all[[:space:]]+tasks[[:space:]]+complete)' "$WORKSPACE/.planning/STATE.md" 2>/dev/null; then
|
|
136
|
+
STATE_MD_COMPLETE=true
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Check 2: Branch pushed to remote (git log origin/<branch>..HEAD is empty)
|
|
141
|
+
if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE/.git" ] || ([ -n "$WORKSPACE" ] && git -C "$WORKSPACE" rev-parse --git-dir >/dev/null 2>&1); then
|
|
142
|
+
BRANCH=$(git -C "$WORKSPACE" branch --show-current 2>/dev/null || echo "")
|
|
143
|
+
if [ -n "$BRANCH" ]; then
|
|
144
|
+
UNPUSHED=$(git -C "$WORKSPACE" log "origin/$BRANCH..HEAD" --oneline 2>/dev/null || echo "no-remote")
|
|
145
|
+
if [ "$UNPUSHED" = "" ]; then
|
|
146
|
+
BRANCH_PUSHED=true
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Check 3: Beads closed (all tasks done)
|
|
152
|
+
if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE/.planning/beads" ]; then
|
|
153
|
+
OPEN_BEADS=$(bd list --status open 2>/dev/null | grep -c '.' || echo "0")
|
|
154
|
+
if [ "$OPEN_BEADS" = "0" ]; then
|
|
155
|
+
BEADS_CLOSED=true
|
|
156
|
+
fi
|
|
157
|
+
elif [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE/.planning/beads" ]; then
|
|
158
|
+
# No beads directory = treat as N/A (closed)
|
|
159
|
+
BEADS_CLOSED=true
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Apply decision matrix from spec
|
|
163
|
+
if [ "$STATE_MD_COMPLETE" = "true" ] && [ "$BRANCH_PUSHED" = "true" ]; then
|
|
164
|
+
# Strong evidence: agent completed but forgot pan work done
|
|
165
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID evidence=FORGOT_COMPLETION (STATE.md=complete, branch=pushed, beads=$BEADS_CLOSED)" \
|
|
166
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
167
|
+
|
|
168
|
+
write_resolution "done"
|
|
169
|
+
|
|
170
|
+
# Record nudge timestamp for cooldown
|
|
171
|
+
mkdir -p "$AGENT_STATE_DIR"
|
|
172
|
+
date +%s > "$NUDGE_FILE" 2>/dev/null || true
|
|
173
|
+
|
|
174
|
+
NUDGE_MSG="Your STATE.md indicates work is complete and your branch is pushed. You MUST run this command now:
|
|
175
|
+
|
|
176
|
+
pan work done $ISSUE_ID -c \"Implementation complete\"
|
|
177
|
+
|
|
178
|
+
If you still have remaining tasks, continue working on them. Do NOT stop until all work is done AND you have called pan work done."
|
|
179
|
+
|
|
180
|
+
TMPFILE=$(mktemp)
|
|
181
|
+
echo "$NUDGE_MSG" > "$TMPFILE"
|
|
182
|
+
tmux load-buffer "$TMPFILE" 2>/dev/null
|
|
183
|
+
tmux paste-buffer -t "$AGENT_ID" 2>/dev/null
|
|
184
|
+
sleep 0.3
|
|
185
|
+
tmux send-keys -t "$AGENT_ID" C-m 2>/dev/null
|
|
186
|
+
rm -f "$TMPFILE"
|
|
187
|
+
|
|
188
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: Sent FORGOT_COMPLETION nudge to $AGENT_ID" \
|
|
189
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
190
|
+
|
|
191
|
+
exit 0
|
|
192
|
+
|
|
193
|
+
elif [ "$STATE_MD_COMPLETE" = "true" ] && [ "$BRANCH_PUSHED" = "false" ]; then
|
|
194
|
+
# STATE.md says done but branch not yet pushed — agent is still working (pushing)
|
|
195
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID evidence=STILL_WORKING (STATE.md=complete, branch=not-pushed)" \
|
|
196
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
60
197
|
exit 0
|
|
61
198
|
fi
|
|
62
199
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
200
|
+
# ============================================================================
|
|
201
|
+
# Heuristic pre-check: skip expensive AI call if clearly mid-work
|
|
202
|
+
# ============================================================================
|
|
203
|
+
OUTPUT=$(tmux capture-pane -t "$AGENT_ID" -p -S -80 2>/dev/null || echo "")
|
|
204
|
+
if [ -z "$OUTPUT" ]; then
|
|
66
205
|
exit 0
|
|
67
206
|
fi
|
|
68
207
|
|
|
@@ -71,8 +210,9 @@ if ! echo "$OUTPUT" | tail -10 | grep -qE '(^❯|Worked for)'; then
|
|
|
71
210
|
exit 0 # Agent doesn't appear to be at an idle prompt
|
|
72
211
|
fi
|
|
73
212
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
213
|
+
# ============================================================================
|
|
214
|
+
# LLM fallback for ambiguous cases
|
|
215
|
+
# ============================================================================
|
|
76
216
|
COMPLETION_MODEL="${PANOPTICON_COMPLETION_CHECK_MODEL:-}"
|
|
77
217
|
if [ -z "$COMPLETION_MODEL" ]; then
|
|
78
218
|
CONFIG_FILE="$HOME/.panopticon/config.yaml"
|
|
@@ -82,7 +222,33 @@ if [ -z "$COMPLETION_MODEL" ]; then
|
|
|
82
222
|
fi
|
|
83
223
|
COMPLETION_MODEL="${COMPLETION_MODEL:-claude-haiku-4-5}"
|
|
84
224
|
|
|
85
|
-
#
|
|
225
|
+
# Read current resolution count from runtime.json for escalation logic
|
|
226
|
+
CURRENT_RESOLUTION=$(python3 -c "
|
|
227
|
+
import json, os
|
|
228
|
+
f = '$AGENT_STATE_DIR/runtime.json'
|
|
229
|
+
if os.path.exists(f):
|
|
230
|
+
try:
|
|
231
|
+
d = json.load(open(f))
|
|
232
|
+
print(d.get('resolution', ''))
|
|
233
|
+
except Exception:
|
|
234
|
+
print('')
|
|
235
|
+
else:
|
|
236
|
+
print('')
|
|
237
|
+
" 2>/dev/null || echo "")
|
|
238
|
+
|
|
239
|
+
CURRENT_COUNT=$(python3 -c "
|
|
240
|
+
import json, os
|
|
241
|
+
f = '$AGENT_STATE_DIR/runtime.json'
|
|
242
|
+
if os.path.exists(f):
|
|
243
|
+
try:
|
|
244
|
+
d = json.load(open(f))
|
|
245
|
+
print(d.get('resolutionCount', 0))
|
|
246
|
+
except Exception:
|
|
247
|
+
print(0)
|
|
248
|
+
else:
|
|
249
|
+
print(0)
|
|
250
|
+
" 2>/dev/null || echo "0")
|
|
251
|
+
|
|
86
252
|
ANALYSIS_PROMPT="You are analyzing a work agent's terminal output to determine if it finished its work but forgot to call 'pan work done'.
|
|
87
253
|
|
|
88
254
|
The agent was working on issue $ISSUE_ID. Here is the last 80 lines of its terminal output:
|
|
@@ -97,25 +263,19 @@ Respond with EXACTLY one of these words (nothing else):
|
|
|
97
263
|
- STOPPED_FOR_INPUT — if the agent stopped because it needs human input or hit a blocker
|
|
98
264
|
- UNCLEAR — if you cannot determine the state"
|
|
99
265
|
|
|
100
|
-
# Run the analysis using claude CLI (headless, no interactive session)
|
|
101
266
|
RESULT=$(echo "$ANALYSIS_PROMPT" | claude -p --model "$COMPLETION_MODEL" --max-tokens 20 2>/dev/null || echo "UNCLEAR")
|
|
102
|
-
|
|
103
|
-
# Extract just the verdict (first word of output, strip whitespace)
|
|
104
267
|
VERDICT=$(echo "$RESULT" | tr -d '[:space:]' | head -c 30)
|
|
105
268
|
|
|
106
|
-
# Log the check
|
|
107
|
-
LOG_DIR="$HOME/.panopticon/logs"
|
|
108
|
-
mkdir -p "$LOG_DIR"
|
|
109
269
|
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID ($ISSUE_ID) -> $VERDICT (model: $COMPLETION_MODEL)" \
|
|
110
270
|
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
111
271
|
|
|
112
272
|
if [ "$VERDICT" = "FORGOT_COMPLETION" ]; then
|
|
273
|
+
write_resolution "done"
|
|
274
|
+
|
|
113
275
|
# Record nudge timestamp for cooldown
|
|
114
|
-
mkdir -p "$
|
|
276
|
+
mkdir -p "$AGENT_STATE_DIR"
|
|
115
277
|
date +%s > "$NUDGE_FILE" 2>/dev/null || true
|
|
116
278
|
|
|
117
|
-
# Write the nudge message to a temp file and use load-buffer + paste-buffer
|
|
118
|
-
# (the reliable tmux message delivery pattern from CLAUDE.md)
|
|
119
279
|
NUDGE_MSG="You stopped without calling pan work done. If your implementation is complete, you MUST run this command now:
|
|
120
280
|
|
|
121
281
|
pan work done $ISSUE_ID -c \"Implementation complete\"
|
|
@@ -130,8 +290,45 @@ If you still have remaining tasks, continue working on them. Do NOT stop until a
|
|
|
130
290
|
tmux send-keys -t "$AGENT_ID" C-m 2>/dev/null
|
|
131
291
|
rm -f "$TMPFILE"
|
|
132
292
|
|
|
133
|
-
echo "[$(date -Iseconds)] work-agent-stop-hook: Sent
|
|
293
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: Sent FORGOT_COMPLETION nudge to $AGENT_ID" \
|
|
134
294
|
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
295
|
+
|
|
296
|
+
elif [ "$VERDICT" = "STOPPED_FOR_INPUT" ]; then
|
|
297
|
+
write_resolution "needs_input"
|
|
298
|
+
|
|
299
|
+
elif [ "$VERDICT" = "UNCLEAR" ]; then
|
|
300
|
+
# Escalate to stuck after 2+ UNCLEAR results
|
|
301
|
+
if [ "$CURRENT_RESOLUTION" = "unclear" ] || [ "$CURRENT_COUNT" -ge 2 ]; then
|
|
302
|
+
write_resolution "stuck"
|
|
303
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID escalated to stuck after $CURRENT_COUNT UNCLEAR results" \
|
|
304
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
305
|
+
else
|
|
306
|
+
# First UNCLEAR — track it but don't escalate yet
|
|
307
|
+
python3 - <<PYEOF 2>/dev/null || true
|
|
308
|
+
import json, os
|
|
309
|
+
|
|
310
|
+
runtime_file = "$AGENT_STATE_DIR/runtime.json"
|
|
311
|
+
data = {}
|
|
312
|
+
if os.path.exists(runtime_file):
|
|
313
|
+
try:
|
|
314
|
+
with open(runtime_file) as f:
|
|
315
|
+
data = json.load(f)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
existing = data.get('resolution', '')
|
|
320
|
+
count = data.get('resolutionCount', 0)
|
|
321
|
+
if existing == 'unclear':
|
|
322
|
+
data['resolutionCount'] = count + 1
|
|
323
|
+
else:
|
|
324
|
+
data['resolution'] = 'unclear'
|
|
325
|
+
data['resolutionCount'] = 1
|
|
326
|
+
data['resolutionUpdatedAt'] = "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
327
|
+
|
|
328
|
+
with open(runtime_file, 'w') as f:
|
|
329
|
+
json.dump(data, f, indent=2)
|
|
330
|
+
PYEOF
|
|
331
|
+
fi
|
|
135
332
|
fi
|
|
136
333
|
|
|
137
334
|
exit 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/projects.ts"],"sourcesContent":["/**\n * Project Registry - Multi-project support for Panopticon\n *\n * Maps Linear team prefixes and labels to project paths for workspace creation.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { PANOPTICON_HOME } from './paths.js';\n\nexport const PROJECTS_CONFIG_FILE = join(PANOPTICON_HOME, 'projects.yaml');\n\n/**\n * Issue routing rule - routes issues with certain labels to specific paths\n */\nexport interface IssueRoutingRule {\n labels?: string[];\n default?: boolean;\n path: string;\n}\n\n/**\n * Workspace configuration (imported from workspace-config.ts for full details)\n */\nexport interface WorkspaceConfig {\n type?: 'polyrepo' | 'monorepo';\n workspaces_dir?: string;\n repos?: Array<{ name: string; path: string; branch_prefix?: string }>;\n dns?: { domain: string; entries: string[]; sync_method?: 'wsl2hosts' | 'hosts_file' | 'dnsmasq' };\n ports?: Record<string, { range: [number, number] }>;\n docker?: { traefik?: string; compose_template?: string };\n database?: { seed_file?: string; container_name?: string; [key: string]: any };\n agent?: { template_dir: string; templates?: Array<{ source: string; target: string }>; copy_dirs?: string[]; symlinks?: string[] };\n env?: { template?: string; secrets_file?: string };\n services?: Array<{ name: string; path: string; start_command: string; docker_command?: string; health_url?: string; port?: number }>;\n}\n\n/**\n * Test configuration\n */\nexport interface TestConfig {\n type: string;\n path: string;\n command: string;\n container?: boolean;\n container_name?: string;\n env?: Record<string, string>;\n}\n\n/**\n * Specialist configuration for per-project specialists\n */\nexport interface SpecialistConfig {\n /** Number of recent runs to include in context digest (default: 5) */\n context_runs?: number;\n /** Model to use for generating context digests (null = same as specialist) */\n digest_model?: string | null;\n /** Log retention policy */\n retention?: {\n /** Maximum days to keep logs */\n max_days: number;\n /** Maximum number of runs to keep (whichever is more permissive) */\n max_runs: number;\n };\n /** Per-specialist prompt overrides */\n prompts?: {\n 'review-agent'?: string;\n 'test-agent'?: string;\n 'merge-agent'?: string;\n };\n}\n\n/**\n * Project configuration\n */\nexport interface ProjectConfig {\n name: string;\n path: string;\n linear_team?: string;\n github_repo?: string; // e.g. \"owner/repo\"\n gitlab_repo?: string; // e.g. \"group/repo\"\n issue_routing?: IssueRoutingRule[];\n /** Workspace configuration */\n workspace?: WorkspaceConfig;\n /** Test configuration by name */\n tests?: Record<string, TestConfig>;\n /** Custom command to create workspaces (e.g., infra/new-feature for MYN) */\n workspace_command?: string;\n /** Custom command to remove workspaces */\n workspace_remove_command?: string;\n /** Rally project OID (e.g., \"/project/822404704163\") for per-project Rally scoping */\n rally_project?: string;\n /** Specialist agent configuration */\n specialists?: SpecialistConfig;\n}\n\n/**\n * Full projects configuration file\n */\nexport interface ProjectsConfig {\n projects: Record<string, ProjectConfig>;\n}\n\n/**\n * Resolved project info for workspace creation\n */\nexport interface ResolvedProject {\n projectKey: string;\n projectName: string;\n projectPath: string;\n linearTeam?: string;\n}\n\n/**\n * Load projects configuration from ~/.panopticon/projects.yaml\n */\nexport function loadProjectsConfig(): ProjectsConfig {\n if (!existsSync(PROJECTS_CONFIG_FILE)) {\n return { projects: {} };\n }\n\n try {\n const content = readFileSync(PROJECTS_CONFIG_FILE, 'utf-8');\n const config = parseYaml(content) as ProjectsConfig;\n return config || { projects: {} };\n } catch (error: any) {\n console.error(`Failed to parse projects.yaml: ${error.message}`);\n return { projects: {} };\n }\n}\n\n/**\n * Save projects configuration\n */\nexport function saveProjectsConfig(config: ProjectsConfig): void {\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const yaml = stringifyYaml(config, { indent: 2 });\n writeFileSync(PROJECTS_CONFIG_FILE, yaml, 'utf-8');\n}\n\n/**\n * Get a list of all registered projects\n */\nexport function listProjects(): Array<{ key: string; config: ProjectConfig }> {\n const config = loadProjectsConfig();\n return Object.entries(config.projects).map(([key, projectConfig]) => ({\n key,\n config: projectConfig,\n }));\n}\n\n/**\n * Add or update a project in the registry\n */\nexport function registerProject(key: string, projectConfig: ProjectConfig): void {\n const config = loadProjectsConfig();\n config.projects[key] = projectConfig;\n saveProjectsConfig(config);\n}\n\n/**\n * Remove a project from the registry\n */\nexport function unregisterProject(key: string): boolean {\n const config = loadProjectsConfig();\n if (config.projects[key]) {\n delete config.projects[key];\n saveProjectsConfig(config);\n return true;\n }\n return false;\n}\n\n/**\n * Extract Linear team prefix from an issue ID\n * E.g., \"MIN-123\" -> \"MIN\", \"PAN-456\" -> \"PAN\"\n */\nexport function extractTeamPrefix(issueId: string): string | null {\n const match = issueId.match(/^([A-Z]+)-\\d+$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\n/**\n * Find project by Linear team prefix\n */\nexport function findProjectByTeam(teamPrefix: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n\n for (const [, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix.toUpperCase()) {\n return projectConfig;\n }\n }\n\n return null;\n}\n\n/**\n * Find project by workspace path.\n * Matches any project whose root path is an ancestor of the given path.\n * Used to resolve the tracker (GitHub/GitLab) from a workspace directory.\n */\nexport function findProjectByPath(workspacePath: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n const normalizedTarget = resolve(workspacePath);\n\n for (const [, projectConfig] of Object.entries(config.projects)) {\n const normalizedProject = resolve(projectConfig.path);\n if (normalizedTarget === normalizedProject || normalizedTarget.startsWith(normalizedProject + '/')) {\n return projectConfig;\n }\n }\n\n return null;\n}\n\n/**\n * Resolve the correct project path for an issue based on labels\n *\n * @param project - The project config\n * @param labels - Array of label names from the Linear issue\n * @returns The resolved path (may differ from project.path based on routing rules)\n */\nexport function resolveProjectPath(project: ProjectConfig, labels: string[] = []): string {\n if (!project.issue_routing || project.issue_routing.length === 0) {\n return project.path;\n }\n\n // Normalize labels to lowercase for comparison\n const normalizedLabels = labels.map(l => l.toLowerCase());\n\n // First, check label-based routing rules\n for (const rule of project.issue_routing) {\n if (rule.labels && rule.labels.length > 0) {\n const ruleLabels = rule.labels.map(l => l.toLowerCase());\n const hasMatch = ruleLabels.some(label => normalizedLabels.includes(label));\n if (hasMatch) {\n return rule.path;\n }\n }\n }\n\n // Then, find default rule\n for (const rule of project.issue_routing) {\n if (rule.default) {\n return rule.path;\n }\n }\n\n // Fall back to project path\n return project.path;\n}\n\n/**\n * Resolve project from an issue ID (and optional labels)\n *\n * @param issueId - Linear issue ID (e.g., \"MIN-123\")\n * @param labels - Optional array of label names\n * @returns Resolved project info or null if not found\n */\nexport function resolveProjectFromIssue(\n issueId: string,\n labels: string[] = []\n): ResolvedProject | null {\n const teamPrefix = extractTeamPrefix(issueId);\n if (!teamPrefix) {\n return null;\n }\n\n const config = loadProjectsConfig();\n\n // Find project by team prefix\n for (const [key, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix) {\n const resolvedPath = resolveProjectPath(projectConfig, labels);\n return {\n projectKey: key,\n projectName: projectConfig.name,\n projectPath: resolvedPath,\n linearTeam: projectConfig.linear_team,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Get a project by key\n */\nexport function getProject(key: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n return config.projects[key] || null;\n}\n\n/**\n * Check if projects.yaml exists and has any projects\n */\nexport function hasProjects(): boolean {\n const config = loadProjectsConfig();\n return Object.keys(config.projects).length > 0;\n}\n\n/**\n * Create a default projects.yaml with example structure\n */\nexport function createDefaultProjectsConfig(): ProjectsConfig {\n const defaultConfig: ProjectsConfig = {\n projects: {\n // Example project - commented out in actual file\n },\n };\n\n return defaultConfig;\n}\n\n/**\n * Initialize projects.yaml with example configuration\n */\nexport function initializeProjectsConfig(): void {\n if (existsSync(PROJECTS_CONFIG_FILE)) {\n console.log(`Projects config already exists at ${PROJECTS_CONFIG_FILE}`);\n return;\n }\n\n const exampleYaml = `# Panopticon Project Registry\n# Maps Linear teams to project paths for workspace creation\n\nprojects:\n # Example: Mind Your Now project\n # myn:\n # name: \"Mind Your Now\"\n # path: /home/user/projects/myn\n # linear_team: MIN\n # issue_routing:\n # # Route docs/marketing issues to docs repo\n # - labels: [docs, marketing, seo, landing-pages]\n # path: /home/user/projects/myn/docs\n # # Default: main repo\n # - default: true\n # path: /home/user/projects/myn\n # specialists:\n # context_runs: 5\n # digest_model: null # Use same model as specialist\n # retention:\n # max_days: 30\n # max_runs: 50\n # prompts:\n # review-agent: |\n # Pay special attention to:\n # - Database migration safety\n # - API backward compatibility\n\n # Example: Panopticon itself\n # panopticon:\n # name: \"Panopticon\"\n # path: /home/user/projects/panopticon\n # linear_team: PAN\n`;\n\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n writeFileSync(PROJECTS_CONFIG_FILE, exampleYaml, 'utf-8');\n console.log(`Created example projects config at ${PROJECTS_CONFIG_FILE}`);\n}\n\n/**\n * Default specialist configuration values\n */\nconst DEFAULT_SPECIALIST_CONFIG: Required<SpecialistConfig> = {\n context_runs: 5,\n digest_model: null,\n retention: {\n max_days: 30,\n max_runs: 50,\n },\n prompts: {},\n};\n\n/**\n * Get specialist configuration for a project with defaults\n *\n * @param projectKey - Project key\n * @returns Specialist config with defaults applied\n */\nexport function getSpecialistConfig(projectKey: string): Required<SpecialistConfig> {\n const project = getProject(projectKey);\n\n if (!project || !project.specialists) {\n return DEFAULT_SPECIALIST_CONFIG;\n }\n\n return {\n context_runs: project.specialists.context_runs ?? DEFAULT_SPECIALIST_CONFIG.context_runs,\n digest_model: project.specialists.digest_model ?? DEFAULT_SPECIALIST_CONFIG.digest_model,\n retention: {\n max_days: project.specialists.retention?.max_days ?? DEFAULT_SPECIALIST_CONFIG.retention.max_days,\n max_runs: project.specialists.retention?.max_runs ?? DEFAULT_SPECIALIST_CONFIG.retention.max_runs,\n },\n prompts: project.specialists.prompts ?? DEFAULT_SPECIALIST_CONFIG.prompts,\n };\n}\n\n/**\n * Get retention policy for a project's specialists\n *\n * @param projectKey - Project key\n * @returns Retention policy\n */\nexport function getSpecialistRetention(projectKey: string): { max_days: number; max_runs: number } {\n const config = getSpecialistConfig(projectKey);\n return config.retention;\n}\n\n/**\n * Find all projects that have a rally_project configured.\n * Returns array of { key, config } for projects with Rally project OIDs.\n */\nexport function findProjectsByRallyProject(): Array<{ key: string; config: ProjectConfig }> {\n const config = loadProjectsConfig();\n return Object.entries(config.projects)\n .filter(([, projectConfig]) => !!projectConfig.rally_project)\n .map(([key, projectConfig]) => ({ key, config: projectConfig }));\n}\n\n/**\n * Get custom prompt override for a specialist (if configured)\n *\n * @param projectKey - Project key\n * @param specialistType - Specialist type\n * @returns Custom prompt or null if not configured\n */\nexport function getSpecialistPromptOverride(\n projectKey: string,\n specialistType: 'review-agent' | 'test-agent' | 'merge-agent'\n): string | null {\n const config = getSpecialistConfig(projectKey);\n return config.prompts[specialistType] || null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/tracker/linear.ts","../src/lib/tracker/github.ts","../src/lib/tracker/gitlab.ts","../src/lib/tracker/factory.ts"],"sourcesContent":["/**\n * Linear Issue Tracker Adapter\n *\n * Implements IssueTracker interface for Linear.\n */\n\nimport { LinearClient } from '@linear/sdk';\nimport type {\n Issue,\n IssueFilters,\n IssueState,\n IssueTracker,\n IssueUpdate,\n NewIssue,\n Comment,\n TrackerType,\n} from './interface.js';\nimport { IssueNotFoundError, TrackerAuthError } from './interface.js';\n\n// Map Linear state types to our normalized states\nconst STATE_MAP: Record<string, IssueState> = {\n backlog: 'open',\n unstarted: 'open',\n started: 'in_progress',\n completed: 'closed',\n canceled: 'closed',\n};\n\nexport class LinearTracker implements IssueTracker {\n readonly name: TrackerType = 'linear';\n private client: LinearClient;\n private defaultTeam?: string;\n\n constructor(apiKey: string, options?: { team?: string }) {\n if (!apiKey) {\n throw new TrackerAuthError('linear', 'API key is required');\n }\n this.client = new LinearClient({ apiKey });\n this.defaultTeam = options?.team;\n }\n\n async listIssues(filters?: IssueFilters): Promise<Issue[]> {\n const team = filters?.team ?? this.defaultTeam;\n\n const result = await this.client.issues({\n first: filters?.limit ?? 50,\n filter: {\n team: team ? { key: { eq: team } } : undefined,\n state: filters?.state\n ? { type: { eq: this.reverseMapState(filters.state) } }\n : filters?.includeClosed\n ? undefined\n : { type: { neq: 'completed' } },\n labels: filters?.labels?.length\n ? { name: { in: filters.labels } }\n : undefined,\n assignee: filters?.assignee\n ? { name: { containsIgnoreCase: filters.assignee } }\n : undefined,\n },\n });\n\n const issues: Issue[] = [];\n for (const node of result.nodes) {\n issues.push(await this.normalizeIssue(node));\n }\n return issues;\n }\n\n async getIssue(id: string): Promise<Issue> {\n try {\n // Check if it's a UUID (36 chars with hyphens)\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);\n\n if (isUuid) {\n // Fetch directly by UUID\n const issue = await this.client.issue(id);\n if (issue) {\n return this.normalizeIssue(issue);\n }\n } else {\n // Parse identifier (e.g., MIN-630) and search\n const match = id.match(/^([A-Z]+)-(\\d+)$/i);\n if (match) {\n const [, teamKey, number] = match;\n // Use searchIssues which supports identifier matching\n const results = await this.client.searchIssues(id, { first: 1 });\n if (results.nodes.length > 0) {\n return this.normalizeIssue(results.nodes[0]);\n }\n }\n }\n\n throw new IssueNotFoundError(id, 'linear');\n } catch (error) {\n if (error instanceof IssueNotFoundError) throw error;\n throw new IssueNotFoundError(id, 'linear');\n }\n }\n\n async updateIssue(id: string, update: IssueUpdate): Promise<Issue> {\n const issue = await this.getIssue(id);\n\n const updatePayload: Record<string, unknown> = {};\n\n if (update.title !== undefined) {\n updatePayload.title = update.title;\n }\n if (update.description !== undefined) {\n updatePayload.description = update.description;\n }\n if (update.priority !== undefined) {\n updatePayload.priority = update.priority;\n }\n if (update.dueDate !== undefined) {\n updatePayload.dueDate = update.dueDate;\n }\n if (update.state !== undefined) {\n // Need to find the state ID - this is complex in Linear\n // For now, we'll use the transition method\n await this.transitionIssue(id, update.state);\n }\n if (update.labels !== undefined) {\n // Need to look up label IDs - complex operation\n // TODO: Implement label updates\n }\n\n if (Object.keys(updatePayload).length > 0) {\n await this.client.updateIssue(issue.id, updatePayload);\n }\n\n return this.getIssue(id);\n }\n\n async createIssue(newIssue: NewIssue): Promise<Issue> {\n const team = newIssue.team ?? this.defaultTeam;\n\n if (!team) {\n throw new Error('Team is required to create an issue');\n }\n\n // Get team ID from key\n const teams = await this.client.teams({\n filter: { key: { eq: team } },\n });\n\n if (teams.nodes.length === 0) {\n throw new Error(`Team not found: ${team}`);\n }\n\n const teamId = teams.nodes[0].id;\n\n const result = await this.client.createIssue({\n teamId,\n title: newIssue.title,\n description: newIssue.description,\n priority: newIssue.priority,\n dueDate: newIssue.dueDate,\n });\n\n const created = await result.issue;\n if (!created) {\n throw new Error('Failed to create issue');\n }\n\n return this.normalizeIssue(created);\n }\n\n async getComments(issueId: string): Promise<Comment[]> {\n const issue = await this.client.issue(issueId);\n const comments = await issue.comments();\n\n return comments.nodes.map((c) => ({\n id: c.id,\n issueId,\n body: c.body,\n author: c.user?.then((u) => u?.name ?? 'Unknown') as unknown as string, // Simplified\n createdAt: c.createdAt.toISOString(),\n updatedAt: c.updatedAt.toISOString(),\n }));\n }\n\n async addComment(issueId: string, body: string): Promise<Comment> {\n const result = await this.client.createComment({\n issueId,\n body,\n });\n\n const comment = await result.comment;\n if (!comment) {\n throw new Error('Failed to create comment');\n }\n\n return {\n id: comment.id,\n issueId,\n body: comment.body,\n author: 'Panopticon', // Simplified\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n };\n }\n\n async transitionIssue(id: string, state: IssueState): Promise<void> {\n // Resolve the Linear issue directly (avoid normalizeIssue which may fail on SDK edge cases)\n let linearIssue: any;\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);\n if (isUuid) {\n linearIssue = await this.client.issue(id);\n } else {\n const results = await this.client.searchIssues(id, { first: 1 });\n if (results.nodes.length > 0) {\n linearIssue = results.nodes[0];\n } else {\n throw new IssueNotFoundError(id, 'linear');\n }\n }\n\n // Get workflow states for the issue's team\n const team = await linearIssue.team;\n if (!team) {\n throw new Error('Could not determine issue team');\n }\n\n const states = await team.states();\n const targetStateType = this.reverseMapState(state);\n\n // Find a state matching the target type.\n // Multiple states can share the same type (e.g., \"In Planning\", \"In Progress\", \"In Review\"\n // are all type \"started\"). Prefer the one with the lowest position (most basic/default state\n // for that type), which matches Linear's convention.\n const matchingStates = states.nodes\n .filter((s: any) => s.type === targetStateType)\n .sort((a: any, b: any) => (a.position ?? 0) - (b.position ?? 0));\n const targetState = matchingStates[0];\n if (!targetState) {\n throw new Error(`No state found matching type: ${targetStateType}`);\n }\n\n await this.client.updateIssue(linearIssue.id, {\n stateId: targetState.id,\n });\n }\n\n async linkPR(issueId: string, prUrl: string): Promise<void> {\n const issue = await this.getIssue(issueId);\n\n await this.client.createAttachment({\n issueId: issue.id,\n title: 'Pull Request',\n url: prUrl,\n });\n }\n\n private async normalizeIssue(linearIssue: any): Promise<Issue> {\n const state = await linearIssue.state;\n const assignee = await linearIssue.assignee;\n const labels = await linearIssue.labels();\n\n // Handle dueDate - can be Date, string, or undefined\n let dueDate: string | undefined;\n if (linearIssue.dueDate) {\n dueDate = linearIssue.dueDate instanceof Date\n ? linearIssue.dueDate.toISOString()\n : String(linearIssue.dueDate);\n }\n\n return {\n id: linearIssue.id,\n ref: linearIssue.identifier,\n title: linearIssue.title,\n description: linearIssue.description ?? '',\n state: this.mapState(state?.type ?? 'backlog'),\n labels: labels?.nodes?.map((l: any) => l.name) ?? [],\n assignee: assignee?.name,\n url: linearIssue.url,\n tracker: 'linear',\n priority: linearIssue.priority,\n dueDate,\n createdAt: linearIssue.createdAt instanceof Date\n ? linearIssue.createdAt.toISOString()\n : String(linearIssue.createdAt),\n updatedAt: linearIssue.updatedAt instanceof Date\n ? linearIssue.updatedAt.toISOString()\n : String(linearIssue.updatedAt),\n };\n }\n\n private mapState(linearState: string): IssueState {\n return STATE_MAP[linearState] ?? 'open';\n }\n\n private reverseMapState(state: IssueState): string {\n switch (state) {\n case 'open':\n return 'unstarted';\n case 'in_progress':\n return 'started';\n case 'closed':\n return 'completed';\n default:\n return 'unstarted';\n }\n }\n}\n","/**\n * GitHub Issues Tracker Adapter\n *\n * Implements IssueTracker interface for GitHub Issues.\n */\n\nimport { Octokit } from '@octokit/rest';\nimport type {\n Issue,\n IssueFilters,\n IssueState,\n IssueTracker,\n IssueUpdate,\n NewIssue,\n Comment,\n TrackerType,\n} from './interface.js';\nimport { IssueNotFoundError, TrackerAuthError } from './interface.js';\n\nexport class GitHubTracker implements IssueTracker {\n readonly name: TrackerType = 'github';\n private octokit: Octokit;\n private owner: string;\n private repo: string;\n\n constructor(token: string, owner: string, repo: string) {\n if (!token) {\n throw new TrackerAuthError('github', 'Token is required');\n }\n if (!owner || !repo) {\n throw new Error('GitHub owner and repo are required');\n }\n\n this.octokit = new Octokit({ auth: token });\n this.owner = owner;\n this.repo = repo;\n }\n\n async listIssues(filters?: IssueFilters): Promise<Issue[]> {\n const state = this.mapStateToGitHub(filters?.state);\n\n const response = await this.octokit.issues.listForRepo({\n owner: this.owner,\n repo: this.repo,\n state: filters?.includeClosed ? 'all' : state,\n labels: filters?.labels?.join(',') || undefined,\n assignee: filters?.assignee || undefined,\n per_page: filters?.limit ?? 50,\n });\n\n // Filter out pull requests (GitHub API returns both)\n const issues = response.data.filter((item) => !item.pull_request);\n\n return issues.map((issue) => this.normalizeIssue(issue));\n }\n\n async getIssue(id: string): Promise<Issue> {\n try {\n // Parse the issue number from refs like \"#42\" or just \"42\"\n const issueNumber = parseInt(id.replace(/^#/, ''), 10);\n\n if (isNaN(issueNumber)) {\n throw new IssueNotFoundError(id, 'github');\n }\n\n const { data: issue } = await this.octokit.issues.get({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n });\n\n return this.normalizeIssue(issue);\n } catch (error: any) {\n if (error?.status === 404) {\n throw new IssueNotFoundError(id, 'github');\n }\n throw error;\n }\n }\n\n async updateIssue(id: string, update: IssueUpdate): Promise<Issue> {\n const issueNumber = parseInt(id.replace(/^#/, ''), 10);\n\n const updatePayload: Record<string, unknown> = {};\n\n if (update.title !== undefined) {\n updatePayload.title = update.title;\n }\n if (update.description !== undefined) {\n updatePayload.body = update.description;\n }\n if (update.state !== undefined) {\n updatePayload.state = update.state === 'closed' ? 'closed' : 'open';\n }\n if (update.labels !== undefined) {\n updatePayload.labels = update.labels;\n }\n if (update.assignee !== undefined) {\n updatePayload.assignees = update.assignee ? [update.assignee] : [];\n }\n\n await this.octokit.issues.update({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n ...updatePayload,\n });\n\n return this.getIssue(id);\n }\n\n async createIssue(newIssue: NewIssue): Promise<Issue> {\n const { data: issue } = await this.octokit.issues.create({\n owner: this.owner,\n repo: this.repo,\n title: newIssue.title,\n body: newIssue.description,\n labels: newIssue.labels,\n assignees: newIssue.assignee ? [newIssue.assignee] : undefined,\n });\n\n return this.normalizeIssue(issue);\n }\n\n async getComments(issueId: string): Promise<Comment[]> {\n const issueNumber = parseInt(issueId.replace(/^#/, ''), 10);\n\n const { data: comments } = await this.octokit.issues.listComments({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n });\n\n return comments.map((c) => ({\n id: String(c.id),\n issueId,\n body: c.body ?? '',\n author: c.user?.login ?? 'Unknown',\n createdAt: c.created_at,\n updatedAt: c.updated_at,\n }));\n }\n\n async addComment(issueId: string, body: string): Promise<Comment> {\n const issueNumber = parseInt(issueId.replace(/^#/, ''), 10);\n\n const { data: comment } = await this.octokit.issues.createComment({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n body,\n });\n\n return {\n id: String(comment.id),\n issueId,\n body: comment.body ?? '',\n author: comment.user?.login ?? 'Unknown',\n createdAt: comment.created_at,\n updatedAt: comment.updated_at,\n };\n }\n\n async transitionIssue(id: string, state: IssueState): Promise<void> {\n const issueNumber = parseInt(id.replace(/^#/, ''), 10);\n\n if (state === 'in_progress') {\n // GitHub has no native \"in progress\" state — use a label instead.\n await this.ensureLabelExists('in-progress', 'In progress', '0075ca');\n await this.octokit.issues.addLabels({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n labels: ['in-progress'],\n });\n } else {\n // Remove in-progress label when moving to open or closed\n const issue = await this.getIssue(id);\n if (issue.labels?.includes('in-progress')) {\n await this.octokit.issues.removeLabel({\n owner: this.owner,\n repo: this.repo,\n issue_number: issueNumber,\n name: 'in-progress',\n }).catch(() => {/* label may not exist, ignore */});\n }\n await this.updateIssue(id, { state });\n }\n }\n\n /** Ensure a label exists in the repo, creating it if needed. */\n private async ensureLabelExists(name: string, description: string, color: string): Promise<void> {\n try {\n await this.octokit.issues.getLabel({ owner: this.owner, repo: this.repo, name });\n } catch {\n await this.octokit.issues.createLabel({\n owner: this.owner,\n repo: this.repo,\n name,\n description,\n color,\n }).catch(() => {/* race condition: another process created it first */});\n }\n }\n\n async linkPR(issueId: string, prUrl: string): Promise<void> {\n // GitHub auto-links PRs that mention issues\n // Add a comment with the PR link\n await this.addComment(\n issueId,\n `Linked Pull Request: ${prUrl}`\n );\n }\n\n private normalizeIssue(ghIssue: any): Issue {\n const labels: string[] = ghIssue.labels.map((l: any) =>\n typeof l === 'string' ? l : l.name\n );\n return {\n id: String(ghIssue.id),\n ref: `#${ghIssue.number}`,\n title: ghIssue.title,\n description: ghIssue.body ?? '',\n state: this.mapStateFromGitHub(ghIssue.state, labels),\n labels,\n assignee: ghIssue.assignee?.login,\n url: ghIssue.html_url,\n tracker: 'github',\n priority: undefined, // GitHub doesn't have priority\n dueDate: undefined, // GitHub doesn't have due dates on issues\n createdAt: ghIssue.created_at,\n updatedAt: ghIssue.updated_at,\n };\n }\n\n private mapStateFromGitHub(ghState: string, labels: string[] = []): IssueState {\n if (ghState === 'closed') return 'closed';\n if (labels.includes('in-progress')) return 'in_progress';\n return 'open';\n }\n\n private mapStateToGitHub(\n state?: IssueState\n ): 'open' | 'closed' | 'all' {\n if (!state) return 'open';\n if (state === 'closed') return 'closed';\n return 'open'; // Both 'open' and 'in_progress' map to 'open'\n }\n}\n","/**\n * GitLab Issues Tracker Adapter (Stub)\n *\n * Placeholder implementation for GitLab Issues support.\n * Full implementation will use @gitbeaker/rest.\n */\n\nimport type {\n Issue,\n IssueFilters,\n IssueState,\n IssueTracker,\n IssueUpdate,\n NewIssue,\n Comment,\n TrackerType,\n} from './interface.js';\nimport { NotImplementedError } from './interface.js';\n\nexport class GitLabTracker implements IssueTracker {\n readonly name: TrackerType = 'gitlab';\n\n constructor(\n private token: string,\n private projectId: string\n ) {\n // Stub - will initialize @gitbeaker client when implemented\n }\n\n async listIssues(_filters?: IssueFilters): Promise<Issue[]> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async getIssue(_id: string): Promise<Issue> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async updateIssue(_id: string, _update: IssueUpdate): Promise<Issue> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async createIssue(_issue: NewIssue): Promise<Issue> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async getComments(_issueId: string): Promise<Comment[]> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async addComment(_issueId: string, _body: string): Promise<Comment> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async transitionIssue(_id: string, _state: IssueState): Promise<void> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n\n async linkPR(_issueId: string, _prUrl: string): Promise<void> {\n throw new NotImplementedError(\n 'GitLab tracker is not yet implemented. Coming soon!'\n );\n }\n}\n","/**\n * Tracker Factory\n *\n * Creates appropriate tracker instances based on configuration.\n */\n\nimport type { IssueTracker, TrackerType } from './interface.js';\nimport { TrackerAuthError } from './interface.js';\nimport { LinearTracker } from './linear.js';\nimport { GitHubTracker } from './github.js';\nimport { GitLabTracker } from './gitlab.js';\nimport { RallyTracker } from './rally.js';\nimport type { TrackersConfig } from '../config.js';\nimport { loadConfig as loadYamlConfig } from '../config-yaml.js';\n\n// Configuration for a single tracker\nexport interface TrackerConfig {\n type: TrackerType;\n\n // Linear-specific\n apiKeyEnv?: string;\n team?: string;\n\n // GitHub-specific\n tokenEnv?: string;\n owner?: string;\n repo?: string;\n\n // GitLab-specific\n projectId?: string;\n\n // Rally-specific\n server?: string;\n workspace?: string;\n project?: string;\n}\n\n// Multi-tracker configuration (re-exported from config.ts)\n// Note: Use TrackersConfig from config.ts for full type with nested configs\n\n/**\n * Get tracker API key from config.yaml (Settings page).\n * This is checked FIRST — env vars are the fallback, not the other way around.\n */\nfunction getTrackerKeyFromConfig(trackerType: TrackerType): string | undefined {\n try {\n const { config: yamlConfig } = loadYamlConfig();\n return yamlConfig.trackerKeys[trackerType];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Create a tracker instance from configuration.\n * Priority: config.yaml (Settings) > environment variable > custom env var name\n */\nexport function createTracker(config: TrackerConfig): IssueTracker {\n switch (config.type) {\n case 'linear': {\n const configKey = getTrackerKeyFromConfig('linear');\n const envKey = config.apiKeyEnv\n ? process.env[config.apiKeyEnv]\n : process.env.LINEAR_API_KEY;\n const apiKey = configKey || envKey;\n\n if (!apiKey) {\n throw new TrackerAuthError(\n 'linear',\n `API key not found. Configure in Settings or set ${config.apiKeyEnv ?? 'LINEAR_API_KEY'} environment variable.`\n );\n }\n\n return new LinearTracker(apiKey, { team: config.team });\n }\n\n case 'github': {\n const configKey = getTrackerKeyFromConfig('github');\n const envToken = config.tokenEnv\n ? process.env[config.tokenEnv]\n : process.env.GITHUB_TOKEN;\n const token = configKey || envToken;\n\n if (!token) {\n throw new TrackerAuthError(\n 'github',\n `Token not found. Configure in Settings or set ${config.tokenEnv ?? 'GITHUB_TOKEN'} environment variable.`\n );\n }\n\n if (!config.owner || !config.repo) {\n throw new Error(\n 'GitHub tracker requires owner and repo configuration'\n );\n }\n\n return new GitHubTracker(token, config.owner, config.repo);\n }\n\n case 'gitlab': {\n const configKey = getTrackerKeyFromConfig('gitlab');\n const envToken = config.tokenEnv\n ? process.env[config.tokenEnv]\n : process.env.GITLAB_TOKEN;\n const token = configKey || envToken;\n\n if (!token) {\n throw new TrackerAuthError(\n 'gitlab',\n `Token not found. Configure in Settings or set ${config.tokenEnv ?? 'GITLAB_TOKEN'} environment variable.`\n );\n }\n\n if (!config.projectId) {\n throw new Error('GitLab tracker requires projectId configuration');\n }\n\n return new GitLabTracker(token, config.projectId);\n }\n\n case 'rally': {\n const configKey = getTrackerKeyFromConfig('rally');\n const envKey = config.apiKeyEnv\n ? process.env[config.apiKeyEnv]\n : process.env.RALLY_API_KEY;\n const apiKey = configKey || envKey;\n\n if (!apiKey) {\n throw new TrackerAuthError(\n 'rally',\n `API key not found. Configure in Settings or set ${config.apiKeyEnv ?? 'RALLY_API_KEY'} environment variable.`\n );\n }\n\n return new RallyTracker({\n apiKey,\n server: config.server,\n workspace: config.workspace,\n project: config.project,\n });\n }\n\n default:\n throw new Error(`Unknown tracker type: ${config.type}`);\n }\n}\n\n/**\n * Create tracker from trackers configuration section\n */\nexport function createTrackerFromConfig(\n trackersConfig: TrackersConfig,\n trackerType: TrackerType\n): IssueTracker {\n const config = trackersConfig[trackerType];\n\n if (!config) {\n throw new Error(\n `No configuration found for tracker: ${trackerType}. Add [trackers.${trackerType}] to config.`\n );\n }\n\n return createTracker({ ...config, type: trackerType });\n}\n\n/**\n * Get the primary tracker from configuration\n */\nexport function getPrimaryTracker(trackersConfig: TrackersConfig): IssueTracker {\n return createTrackerFromConfig(trackersConfig, trackersConfig.primary);\n}\n\n/**\n * Get the secondary tracker from configuration (if configured)\n */\nexport function getSecondaryTracker(\n trackersConfig: TrackersConfig\n): IssueTracker | null {\n if (!trackersConfig.secondary) {\n return null;\n }\n return createTrackerFromConfig(trackersConfig, trackersConfig.secondary);\n}\n\n/**\n * Get all configured trackers\n */\nexport function getAllTrackers(trackersConfig: TrackersConfig): IssueTracker[] {\n const trackers: IssueTracker[] = [getPrimaryTracker(trackersConfig)];\n\n const secondary = getSecondaryTracker(trackersConfig);\n if (secondary) {\n trackers.push(secondary);\n }\n\n return trackers;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/workspace-config.ts"],"sourcesContent":["/**\n * Workspace Configuration Types\n *\n * Defines the schema for project workspace configuration in projects.yaml\n */\n\nexport interface RepoConfig {\n /** Name of the repo in the workspace (e.g., 'fe', 'api') */\n name: string;\n /** Path to source repo relative to project root */\n path: string;\n /** Branch prefix for feature branches (default: 'feature/') */\n branch_prefix?: string;\n /** Default branch to create feature branches from (default: 'main') */\n default_branch?: string;\n}\n\nexport interface DnsConfig {\n /** Base domain (e.g., 'myn.test') */\n domain: string;\n /**\n * DNS entry patterns. Supports placeholders:\n * - {{FEATURE_FOLDER}}: e.g., 'feature-min-123'\n * - {{FEATURE_NAME}}: e.g., 'min-123'\n * - {{DOMAIN}}: the domain value\n */\n entries: string[];\n /** How to sync DNS: 'wsl2hosts' | 'hosts_file' | 'dnsmasq' */\n sync_method?: 'wsl2hosts' | 'hosts_file' | 'dnsmasq';\n}\n\nexport interface PortConfig {\n /** Port range [start, end] */\n range: [number, number];\n}\n\nexport interface DockerConfig {\n /** Path to Traefik compose file (relative to project root) */\n traefik?: string;\n /** Path to devcontainer template directory */\n compose_template?: string;\n}\n\nexport interface AgentTemplateConfig {\n /** Path to agent template directory */\n template_dir: string;\n /** Files to process with placeholder replacement */\n templates?: Array<{\n source: string;\n target: string;\n }>;\n /** Directories to copy from project template into workspace */\n copy_dirs?: string[];\n /** @deprecated Use copy_dirs instead */\n symlinks?: string[];\n}\n\nexport interface EnvConfig {\n /** Environment variable template with placeholders */\n template?: string;\n /** Additional env vars from secrets */\n secrets_file?: string;\n}\n\nexport interface ServiceConfig {\n /** Service name (e.g., 'api', 'frontend') */\n name: string;\n /** Path relative to workspace (e.g., 'api', 'fe') */\n path: string;\n /** Command to start the service natively (e.g., './run-dev.sh', 'pnpm start') */\n start_command: string;\n /** Command to start inside Docker container (if different) */\n docker_command?: string;\n /** Health check URL pattern (supports placeholders) */\n health_url?: string;\n /** Port the service runs on */\n port?: number;\n}\n\nexport interface TestConfig {\n /** Test type: 'maven' | 'vitest' | 'playwright' | 'jest' | 'pytest' | 'cargo' */\n type: string;\n /** Path to test directory (relative to workspace) */\n path: string;\n /** Command to run tests */\n command: string;\n /** Run inside container for feature workspaces */\n container?: boolean;\n /** Container name pattern (uses {{FEATURE_FOLDER}}) */\n container_name?: string;\n /** Additional environment variables */\n env?: Record<string, string>;\n}\n\nexport interface DatabaseConfig {\n /** Path to seed file for database initialization */\n seed_file?: string;\n /** Command to run after loading seed (e.g., sanitization script) */\n seed_command?: string;\n /** Command to create snapshots from external source (e.g., kubectl exec pg_dump) */\n snapshot_command?: string;\n /** External database connection for direct access */\n external_db?: {\n host: string;\n port?: number;\n database: string;\n user?: string;\n /** Environment variable name containing password */\n password_env?: string;\n };\n /** Container name pattern (supports {{PROJECT}} placeholder) */\n container_name?: string;\n /** Migration tool configuration */\n migrations?: {\n type: 'flyway' | 'liquibase' | 'prisma' | 'typeorm' | 'custom';\n path?: string;\n command?: string;\n };\n}\n\nexport interface TunnelHostname {\n /** Hostname pattern (supports {{FEATURE_FOLDER}} etc.) e.g., \"api-{{FEATURE_FOLDER}}.mindyournow.com\" */\n pattern: string;\n /** HTTP Host header for Traefik routing e.g., \"api-{{FEATURE_FOLDER}}.myn.localhost\" */\n http_host_header?: string;\n /** Skip TLS verification for local dev (default: true) */\n no_tls_verify?: boolean;\n}\n\nexport interface TunnelConfig {\n /** Tunnel provider (currently only Cloudflare) */\n provider: 'cloudflare';\n /** Cloudflare tunnel ID */\n tunnel_id: string;\n /** Cloudflare account ID */\n account_id: string;\n /** Cloudflare zone ID */\n zone_id: string;\n /** Path to credentials file (cert.pem) containing API token */\n credentials_file: string;\n /** Service target for ingress rules (e.g., \"https://localhost\") */\n service_target: string;\n /** Hostnames to create ingress rules + DNS records for */\n hostnames: TunnelHostname[];\n}\n\nexport interface HumeConfig {\n /** Env var name containing the Hume API key (default: HUME_API_KEY) */\n api_key_env?: string;\n /** Config ID of the production/template config to clone from */\n template_config_id: string;\n /** Config name pattern for workspaces (supports placeholders) */\n name_pattern: string;\n /** BYOLLM callback URL pattern (supports placeholders) */\n byollm_url_pattern: string;\n}\n\nexport interface WorkspaceConfig {\n /** Workspace type: 'polyrepo' (multiple git repos) or 'monorepo' (single repo, default) */\n type?: 'polyrepo' | 'monorepo';\n /** Where to create workspaces (relative to project path) */\n workspaces_dir?: string;\n /** Default branch for all repos (default: 'main'). Can be overridden per-repo. */\n default_branch?: string;\n /** Git repositories to include (for polyrepo) */\n repos?: RepoConfig[];\n /** DNS configuration */\n dns?: DnsConfig;\n /** Port assignments for services */\n ports?: Record<string, PortConfig>;\n /** Docker configuration */\n docker?: DockerConfig;\n /** Database seeding configuration */\n database?: DatabaseConfig;\n /** Agent configuration templates */\n agent?: AgentTemplateConfig;\n /** Environment variables */\n env?: EnvConfig;\n /** Service definitions for startup commands */\n services?: ServiceConfig[];\n /** Cloudflare tunnel configuration for external access */\n tunnel?: TunnelConfig;\n /** Hume EVI config management for workspace lifecycle */\n hume?: HumeConfig;\n /** PRD directory path (relative to project path, default: 'docs/prds') */\n prdDir?: string;\n}\n\nexport interface TestsConfig {\n [name: string]: TestConfig;\n}\n\nexport interface ProjectConfig {\n name: string;\n path: string;\n linear_team?: string;\n github_repo?: string;\n gitlab_repo?: string;\n\n /** Workspace configuration */\n workspace?: WorkspaceConfig;\n\n /** Test configuration */\n tests?: TestsConfig;\n\n /** Issue routing rules */\n issue_routing?: Array<{\n labels?: string[];\n path: string;\n default?: boolean;\n }>;\n\n /** Legacy: custom workspace command (deprecated, use workspace config) */\n workspace_command?: string;\n workspace_remove_command?: string;\n}\n\nexport interface ProjectsConfig {\n projects: Record<string, ProjectConfig>;\n}\n\n/**\n * Template placeholders that can be used in configuration\n */\nexport interface TemplatePlaceholders {\n FEATURE_NAME: string; // e.g., 'min-123'\n FEATURE_FOLDER: string; // e.g., 'feature-min-123'\n BRANCH_NAME: string; // e.g., 'feature/min-123'\n COMPOSE_PROJECT: string; // e.g., 'myn-feature-min-123'\n DOMAIN: string; // e.g., 'myn.test'\n PROJECT_NAME: string; // e.g., 'myn'\n PROJECT_PATH: string; // e.g., '/home/user/projects/myn'\n WORKSPACE_PATH: string; // e.g., '/home/user/projects/myn/workspaces/feature-min-123'\n HOME?: string; // e.g., '/home/user' (for docker-compose path sanitization)\n}\n\n/**\n * Replace template placeholders in a string\n */\nexport function replacePlaceholders(template: string, placeholders: TemplatePlaceholders): string {\n let result = template;\n for (const [key, value] of Object.entries(placeholders)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${key}\\\\}\\\\}`, 'g'), value);\n }\n return result;\n}\n\n/**\n * Get default workspace config for a monorepo project\n */\nexport function getDefaultWorkspaceConfig(): WorkspaceConfig {\n return {\n type: 'monorepo',\n workspaces_dir: 'workspaces',\n };\n}\n\n/**\n * Service templates for common project types\n * These provide sensible defaults that can be overridden\n */\nexport const SERVICE_TEMPLATES: Record<string, Partial<ServiceConfig>> = {\n // Frontend frameworks\n 'react': {\n start_command: 'npm start',\n docker_command: 'npm start',\n port: 3000,\n },\n 'react-vite': {\n start_command: 'npm run dev',\n docker_command: 'npm run dev',\n port: 5173,\n },\n 'react-pnpm': {\n start_command: 'pnpm start',\n docker_command: 'pnpm start',\n port: 3000,\n },\n 'nextjs': {\n start_command: 'npm run dev',\n docker_command: 'npm run dev',\n port: 3000,\n },\n 'vue': {\n start_command: 'npm run dev',\n docker_command: 'npm run dev',\n port: 5173,\n },\n 'angular': {\n start_command: 'ng serve',\n docker_command: 'ng serve',\n port: 4200,\n },\n\n // Backend frameworks\n 'spring-boot-maven': {\n start_command: './mvnw spring-boot:run',\n docker_command: './mvnw spring-boot:run',\n port: 8080,\n },\n 'spring-boot-gradle': {\n start_command: './gradlew bootRun',\n docker_command: './gradlew bootRun',\n port: 8080,\n },\n 'express': {\n start_command: 'npm start',\n docker_command: 'npm start',\n port: 3000,\n },\n 'fastapi': {\n start_command: 'uvicorn main:app --reload',\n docker_command: 'uvicorn main:app --host 0.0.0.0 --reload',\n port: 8000,\n },\n 'django': {\n start_command: 'python manage.py runserver',\n docker_command: 'python manage.py runserver 0.0.0.0:8000',\n port: 8000,\n },\n 'rails': {\n start_command: 'rails server',\n docker_command: 'rails server -b 0.0.0.0',\n port: 3000,\n },\n 'go': {\n start_command: 'go run .',\n docker_command: 'go run .',\n port: 8080,\n },\n 'rust-cargo': {\n start_command: 'cargo run',\n docker_command: 'cargo run',\n port: 8080,\n },\n};\n\n/**\n * Get service config from template with overrides\n */\nexport function getServiceFromTemplate(\n templateName: string,\n overrides: Partial<ServiceConfig>\n): ServiceConfig {\n const template = SERVICE_TEMPLATES[templateName] || {};\n return {\n name: overrides.name || templateName,\n path: overrides.path || '.',\n start_command: overrides.start_command || template.start_command || 'npm start',\n docker_command: overrides.docker_command || template.docker_command,\n health_url: overrides.health_url,\n port: overrides.port || template.port,\n };\n}\n"],"mappings":";;;;;;AA+OO,SAAS,oBAAoB,UAAkB,cAA4C;AAChG,MAAI,SAAS;AACb,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,aAAS,OAAO,QAAQ,IAAI,OAAO,SAAS,GAAG,UAAU,GAAG,GAAG,KAAK;AAAA,EACtE;AACA,SAAO;AACT;AAKO,SAAS,4BAA6C;AAC3D,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB;AAAA,EAClB;AACF;AA/PA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}
|