panopticon-cli 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/{agents-5OPQKM5K.js → agents-DMPT32H7.js} +7 -6
  2. package/dist/{chunk-OWHXCGVO.js → chunk-2V2DQ3IX.js} +378 -101
  3. package/dist/chunk-2V2DQ3IX.js.map +1 -0
  4. package/dist/{chunk-F5555J3A.js → chunk-4HST45MO.js} +13 -27
  5. package/dist/chunk-4HST45MO.js.map +1 -0
  6. package/dist/{chunk-YLPSQAM2.js → chunk-565HZ6VV.js} +2 -2
  7. package/dist/chunk-6N2KBSJA.js +452 -0
  8. package/dist/chunk-6N2KBSJA.js.map +1 -0
  9. package/dist/{chunk-VHKSS7QX.js → chunk-D67AQTHF.js} +21 -19
  10. package/dist/chunk-D67AQTHF.js.map +1 -0
  11. package/dist/{chunk-4YSYJ4HM.js → chunk-DFNVHK3N.js} +2 -2
  12. package/dist/{chunk-7SN4L4PH.js → chunk-HOGYHJ2G.js} +2 -2
  13. package/dist/{chunk-FTCPTHIJ.js → chunk-HRU7S4TA.js} +24 -7
  14. package/dist/chunk-HRU7S4TA.js.map +1 -0
  15. package/dist/{chunk-NLQRED36.js → chunk-KBHRXV5T.js} +3 -3
  16. package/dist/chunk-KBHRXV5T.js.map +1 -0
  17. package/dist/{chunk-CWELWPWQ.js → chunk-MOPGR3CL.js} +1 -1
  18. package/dist/chunk-MOPGR3CL.js.map +1 -0
  19. package/dist/{chunk-2V4NF7J2.js → chunk-RLZQB7HS.js} +2 -2
  20. package/dist/chunk-RLZQB7HS.js.map +1 -0
  21. package/dist/{chunk-76F6DSVS.js → chunk-T7BBPDEJ.js} +11 -7
  22. package/dist/chunk-T7BBPDEJ.js.map +1 -0
  23. package/dist/chunk-USYP2SBE.js +317 -0
  24. package/dist/chunk-USYP2SBE.js.map +1 -0
  25. package/dist/{chunk-JM6V62LT.js → chunk-ZDNQFWR5.js} +2 -2
  26. package/dist/{chunk-JM6V62LT.js.map → chunk-ZDNQFWR5.js.map} +1 -1
  27. package/dist/{chunk-HJSM6E6U.js → chunk-ZP6EWSZV.js} +29 -322
  28. package/dist/chunk-ZP6EWSZV.js.map +1 -0
  29. package/dist/{chunk-PELXV435.js → chunk-ZTYHZMEC.js} +2 -2
  30. package/dist/chunk-ZTYHZMEC.js.map +1 -0
  31. package/dist/cli/index.js +1381 -777
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/config-yaml-OVZLKFMA.js +18 -0
  34. package/dist/dashboard/prompts/merge-agent.md +7 -5
  35. package/dist/dashboard/prompts/review-agent.md +12 -1
  36. package/dist/dashboard/prompts/test-agent.md +3 -1
  37. package/dist/dashboard/public/assets/index-BJKEp64j.css +32 -0
  38. package/dist/dashboard/public/assets/index-CgJjqjAV.js +767 -0
  39. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  40. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  41. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  42. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  43. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  44. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  45. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  46. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  47. package/dist/dashboard/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  48. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  49. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  50. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-BflQw4A9.woff +0 -0
  51. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
  52. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  53. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  54. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  55. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  56. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-DxxdqCpr.woff2 +0 -0
  57. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-VcznFIpX.woff +0 -0
  58. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  59. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  60. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  61. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  62. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-D6zpsUhD.woff +0 -0
  63. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-DUi7WF5p.woff2 +0 -0
  64. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  65. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  66. package/dist/dashboard/public/index.html +5 -3
  67. package/dist/dashboard/server.js +4647 -2723
  68. package/dist/{feedback-writer-VRMMWWTW.js → feedback-writer-T43PI5S2.js} +2 -2
  69. package/dist/{hume-WMAUBBV2.js → hume-CKJJ3OUU.js} +3 -3
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/{projects-CFX3RTDL.js → projects-KVM3MN3Y.js} +2 -2
  73. package/dist/{remote-agents-TFSMW7GN.js → remote-agents-ULPD6C5U.js} +3 -3
  74. package/dist/{remote-workspace-7FPGF2RM.js → remote-workspace-XX6ARE6I.js} +3 -3
  75. package/dist/{review-status-TDPSOU5J.js → review-status-XKUKZF6J.js} +3 -2
  76. package/dist/{specialist-context-WGUUYDWY.js → specialist-context-53AWO6AE.js} +6 -5
  77. package/dist/{specialist-context-WGUUYDWY.js.map → specialist-context-53AWO6AE.js.map} +1 -1
  78. package/dist/{specialist-logs-XJB5TCKJ.js → specialist-logs-QREUJ4HN.js} +6 -5
  79. package/dist/{specialists-5LBRHYFA.js → specialists-2DBBXRCK.js} +6 -5
  80. package/dist/{traefik-WFMQX2LY.js → traefik-5GL3Q7DJ.js} +3 -3
  81. package/dist/{tunnel-W2GZBLEV.js → tunnel-BKC7KLBX.js} +3 -3
  82. package/dist/{workspace-manager-E434Z45T.js → workspace-manager-ALBR62AS.js} +5 -5
  83. package/dist/workspace-manager-ALBR62AS.js.map +1 -0
  84. package/package.json +1 -1
  85. package/scripts/record-cost-event.js +8424 -42
  86. package/scripts/work-agent-stop-hook +221 -24
  87. package/dist/chunk-2V4NF7J2.js.map +0 -1
  88. package/dist/chunk-76F6DSVS.js.map +0 -1
  89. package/dist/chunk-CWELWPWQ.js.map +0 -1
  90. package/dist/chunk-F5555J3A.js.map +0 -1
  91. package/dist/chunk-FTCPTHIJ.js.map +0 -1
  92. package/dist/chunk-HJSM6E6U.js.map +0 -1
  93. package/dist/chunk-NLQRED36.js.map +0 -1
  94. package/dist/chunk-OWHXCGVO.js.map +0 -1
  95. package/dist/chunk-PELXV435.js.map +0 -1
  96. package/dist/chunk-VHKSS7QX.js.map +0 -1
  97. package/dist/chunk-YGJ54GW2.js +0 -96
  98. package/dist/chunk-YGJ54GW2.js.map +0 -1
  99. package/dist/dashboard/public/assets/index-Ce6q21Fm.js +0 -743
  100. package/dist/dashboard/public/assets/index-NzpI0ItZ.css +0 -32
  101. package/dist/git-utils-I2UDKNZH.js +0 -131
  102. package/dist/git-utils-I2UDKNZH.js.map +0 -1
  103. /package/dist/{agents-5OPQKM5K.js.map → agents-DMPT32H7.js.map} +0 -0
  104. /package/dist/{chunk-YLPSQAM2.js.map → chunk-565HZ6VV.js.map} +0 -0
  105. /package/dist/{chunk-4YSYJ4HM.js.map → chunk-DFNVHK3N.js.map} +0 -0
  106. /package/dist/{chunk-7SN4L4PH.js.map → chunk-HOGYHJ2G.js.map} +0 -0
  107. /package/dist/{hume-WMAUBBV2.js.map → config-yaml-OVZLKFMA.js.map} +0 -0
  108. /package/dist/{feedback-writer-VRMMWWTW.js.map → feedback-writer-T43PI5S2.js.map} +0 -0
  109. /package/dist/{projects-CFX3RTDL.js.map → hume-CKJJ3OUU.js.map} +0 -0
  110. /package/dist/{remote-agents-TFSMW7GN.js.map → projects-KVM3MN3Y.js.map} +0 -0
  111. /package/dist/{review-status-TDPSOU5J.js.map → remote-agents-ULPD6C5U.js.map} +0 -0
  112. /package/dist/{remote-workspace-7FPGF2RM.js.map → remote-workspace-XX6ARE6I.js.map} +0 -0
  113. /package/dist/{specialist-logs-XJB5TCKJ.js.map → review-status-XKUKZF6J.js.map} +0 -0
  114. /package/dist/{specialists-5LBRHYFA.js.map → specialist-logs-QREUJ4HN.js.map} +0 -0
  115. /package/dist/{traefik-WFMQX2LY.js.map → specialists-2DBBXRCK.js.map} +0 -0
  116. /package/dist/{tunnel-W2GZBLEV.js.map → traefik-5GL3Q7DJ.js.map} +0 -0
  117. /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 a lightweight AI model to analyze the last N lines of terminal output
6
- # and determine if the agent completed its work but failed to signal completion.
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="$HOME/.panopticon/agents/$AGENT_ID/completed"
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="$HOME/.panopticon/agents/$AGENT_ID/.last-completion-nudge"
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
- # Capture the last 80 lines of terminal output
58
- OUTPUT=$(tmux capture-pane -t "$AGENT_ID" -p -S -80 2>/dev/null || echo "")
59
- if [ -z "$OUTPUT" ]; then
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
- # Quick heuristic pre-check: skip the expensive AI call if the output clearly
64
- # shows the agent is mid-work (e.g., running tests, editing files, reading)
65
- if echo "$OUTPUT" | grep -qE '(Reading|Editing|Searching|Running|Compiling|Building|Installing|●.*Bash|●.*Read|●.*Edit|●.*Grep|●.*Write|●.*Glob)' | tail -5 | grep -qE '●'; then
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
- # Determine model to use for completion check
75
- # Priority: env var > config.yaml override > default
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
- # Build the analysis prompt
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 "$(dirname "$NUDGE_FILE")"
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 completion nudge to $AGENT_ID" \
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":[]}