tlc-claude-code 2.9.0 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/tlc/build.md +59 -24
- package/.claude/hooks/tlc-session-init.sh +238 -149
- package/package.json +1 -1
|
@@ -174,50 +174,85 @@ If orchestrator is down, dispatch directly via tmux on the host:
|
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
176
|
tmux new-session -d -s <task-id> -c $(pwd)
|
|
177
|
-
tmux send-keys -t <task-id> "/opt/homebrew/bin/codex --
|
|
177
|
+
tmux send-keys -t <task-id> "/opt/homebrew/bin/codex --dangerously-bypass-approvals-and-sandbox '<prompt>'" Enter
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
### Step O4: Report and
|
|
180
|
+
### Step O4: Report and Monitor
|
|
181
181
|
|
|
182
|
-
After dispatching all tasks, report:
|
|
182
|
+
After dispatching all tasks, report the initial state:
|
|
183
183
|
|
|
184
184
|
```
|
|
185
185
|
Dispatched N tasks to orchestrator:
|
|
186
|
-
ses_abc123 Task 1: Create schema codex running
|
|
187
|
-
ses_def456 Task 2: Add validation codex running
|
|
186
|
+
ses_abc123 Task 1: Create schema codex running
|
|
187
|
+
ses_def456 Task 2: Add validation codex running
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
I'm free — what would you like to work on?
|
|
189
|
+
Monitoring for completion...
|
|
191
190
|
```
|
|
192
191
|
|
|
193
|
-
|
|
192
|
+
**Do NOT stop here. Do NOT say "I'm free". Enter the monitoring loop immediately.**
|
|
194
193
|
|
|
195
|
-
|
|
194
|
+
### Step O5: Auto-Monitor Loop (Mandatory)
|
|
196
195
|
|
|
197
|
-
|
|
196
|
+
After dispatching, poll all sessions every 30 seconds until all reach terminal states. This loop runs automatically — do not wait for the user to ask.
|
|
198
197
|
|
|
199
|
-
|
|
198
|
+
```
|
|
199
|
+
Poll cycle:
|
|
200
|
+
1. For each dispatched session, check: GET /sessions/:id/status (or tmux capture-pane)
|
|
201
|
+
2. Classify each session:
|
|
202
|
+
- completed → ready for merge-back
|
|
203
|
+
- stuck → send Enter once, re-check next cycle. After 3 attempts, ask user: "Session stuck. Retry or cancel?"
|
|
204
|
+
- budget_exhausted → ask user: "Budget exhausted. Re-dispatch or skip?"
|
|
205
|
+
- crashed / errored → log error, mark task as failed, continue with others
|
|
206
|
+
- timed_out → log timeout, mark task as failed
|
|
207
|
+
- running → continue monitoring
|
|
208
|
+
3. When ANY session completes, report immediately: "✅ ses_abc123 Task 1 complete"
|
|
209
|
+
4. When ALL dispatched sessions reach terminal states, exit the loop
|
|
210
|
+
```
|
|
200
211
|
|
|
201
|
-
|
|
202
|
-
# Check all sessions
|
|
203
|
-
curl -s http://localhost:3100/sessions | node -e "
|
|
204
|
-
const d=require('fs').readFileSync('/dev/stdin','utf8');
|
|
205
|
-
JSON.parse(d).forEach(s=>console.log(s.id, s.status, s.project));
|
|
206
|
-
"
|
|
212
|
+
**Use Bash `sleep 30` between poll cycles. Do not use background tasks for this — poll sequentially in the main conversation so results are visible.**
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
Surface progress at natural milestones:
|
|
215
|
+
- When a session completes: one-line status update
|
|
216
|
+
- When all sessions in a parallel batch complete: summary + trigger next batch
|
|
217
|
+
- When a session fails: immediate report with error details
|
|
211
218
|
|
|
212
|
-
|
|
219
|
+
### Step O5a: Dispatch Dependent Tasks
|
|
220
|
+
|
|
221
|
+
When a batch of tasks completes, check the dependency graph for newly unblocked tasks. Dispatch them immediately without asking.
|
|
213
222
|
|
|
214
|
-
```bash
|
|
215
|
-
docker exec tlc-agent-runner tmux send-keys -t <session_id> Enter
|
|
216
223
|
```
|
|
224
|
+
Example:
|
|
225
|
+
Tasks 1+2 dispatched (parallel, no deps)
|
|
226
|
+
Task 1 completes → check if any task depends only on Task 1 → dispatch it
|
|
227
|
+
Task 2 completes → check if remaining tasks are unblocked → dispatch them
|
|
228
|
+
Tasks 3+4 now unblocked → dispatch immediately
|
|
229
|
+
...continue until all tasks dispatched and completed
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Step O5b: Auto Merge-Back
|
|
233
|
+
|
|
234
|
+
When all tasks in the phase complete (or all non-failed tasks complete):
|
|
235
|
+
|
|
236
|
+
1. Pull all commits from container worktrees to the host phase branch
|
|
237
|
+
2. Run the full test suite: `cd server && npx vitest run` (or project's test command)
|
|
238
|
+
3. If tests pass → continue to Step O5c
|
|
239
|
+
4. If tests fail → report failures, attempt auto-fix (one round), re-run tests
|
|
240
|
+
|
|
241
|
+
### Step O5c: Auto Review + PR
|
|
242
|
+
|
|
243
|
+
After tests pass:
|
|
244
|
+
|
|
245
|
+
1. Run auto-review (same checks as Step 10 in Inline Mode)
|
|
246
|
+
2. If review passes → push branch and create PR (ask user before pushing per project rules)
|
|
247
|
+
3. If review fails → fix issues, re-run tests, re-review (max 3 iterations)
|
|
217
248
|
|
|
218
249
|
### Step O6: Cleanup
|
|
219
250
|
|
|
220
|
-
|
|
251
|
+
After PR is created (or phase completes):
|
|
252
|
+
- Remove `.tlc/.build-routing-active`
|
|
253
|
+
- Remove `.tlc/.active-sessions.json`
|
|
254
|
+
- Mark completed tasks `[x]` in PLAN.md
|
|
255
|
+
- Report final summary
|
|
221
256
|
|
|
222
257
|
## INLINE MODE
|
|
223
258
|
|
|
@@ -1,86 +1,91 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# TLC
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# 3. Probe LLM providers and write persistent router state
|
|
2
|
+
# TLC Session Init — Comprehensive Health Check
|
|
3
|
+
# Verifies ALL systems and reports status clearly.
|
|
4
|
+
# No silent failures. No partial checks.
|
|
6
5
|
|
|
7
6
|
if [ ! -f ".tlc.json" ]; then
|
|
8
7
|
exit 0
|
|
9
8
|
fi
|
|
10
9
|
|
|
10
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
11
|
+
TLC_PORT="${TLC_PORT:-3147}"
|
|
12
|
+
TLC_CORE_PORT="${TLC_CORE_PORT:-3100}"
|
|
13
|
+
ERRORS=0
|
|
14
|
+
WARNINGS=0
|
|
15
|
+
|
|
11
16
|
echo "TLC project detected. All work goes through /tlc commands. Run /tlc for current status and next action."
|
|
17
|
+
echo ""
|
|
18
|
+
echo "── System Health Check ──────────────────────────────"
|
|
12
19
|
|
|
13
|
-
# ─── TLC
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
LATEST=$(grep '^latest=' "$UPDATE_FILE" | cut -d= -f2)
|
|
18
|
-
CMD=$(grep '^command=' "$UPDATE_FILE" | cut -d= -f2-)
|
|
19
|
-
if [ -n "$CURRENT" ] && [ -n "$LATEST" ]; then
|
|
20
|
-
echo "TLC update available: ${CURRENT} → ${LATEST}. Run: ${CMD}"
|
|
21
|
-
fi
|
|
22
|
-
fi
|
|
20
|
+
# ─── 1. TLC Package Version ──────────────────────────────
|
|
21
|
+
INSTALLED_VER=$(node -e "try{console.log(require('tlc-claude-code/package.json').version)}catch{try{console.log(require('$PROJECT_DIR/package.json').version)}catch{console.log('not-installed')}}" 2>/dev/null)
|
|
22
|
+
LATEST_VER=$(npm view tlc-claude-code version 2>/dev/null || echo "unknown")
|
|
23
|
+
PROJECT_VER=$(node -e "try{console.log(require('./.tlc.json').tlcVersion||'unknown')}catch{console.log('unknown')}" 2>/dev/null)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
if [ "$INSTALLED_VER" = "not-installed" ]; then
|
|
26
|
+
echo " TLC Package: ❌ not installed (run: npm i -g tlc-claude-code)"
|
|
27
|
+
ERRORS=$((ERRORS + 1))
|
|
28
|
+
elif [ "$INSTALLED_VER" != "$LATEST_VER" ] && [ "$LATEST_VER" != "unknown" ]; then
|
|
29
|
+
echo " TLC Package: ⚠️ ${INSTALLED_VER} (latest: ${LATEST_VER}) — run: npm i -g tlc-claude-code"
|
|
30
|
+
WARNINGS=$((WARNINGS + 1))
|
|
31
|
+
else
|
|
32
|
+
echo " TLC Package: ✅ v${INSTALLED_VER} (latest)"
|
|
33
|
+
fi
|
|
25
34
|
|
|
26
|
-
# ─── TLC
|
|
35
|
+
# ─── 2. TLC Memory System ────────────────────────────────
|
|
36
|
+
mkdir -p "$PROJECT_DIR/.tlc/memory/team/decisions" \
|
|
37
|
+
"$PROJECT_DIR/.tlc/memory/team/gotchas" \
|
|
38
|
+
"$PROJECT_DIR/.tlc/memory/.local/sessions" 2>/dev/null
|
|
27
39
|
|
|
28
|
-
TLC_PORT="${TLC_PORT:-3147}"
|
|
29
40
|
if curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1; then
|
|
30
|
-
|
|
41
|
+
echo " TLC Server: ✅ running (:${TLC_PORT})"
|
|
42
|
+
# Drain spool
|
|
43
|
+
SPOOL_DRAIN=""
|
|
44
|
+
if node -e "require('tlc-claude-code/server/lib/capture/spool-processor')" 2>/dev/null; then
|
|
45
|
+
SPOOL_DRAIN="require('tlc-claude-code/server/lib/capture/spool-processor').processSpool('$PROJECT_DIR')"
|
|
46
|
+
elif [ -f "$PROJECT_DIR/server/lib/capture/spool-processor.js" ]; then
|
|
47
|
+
SPOOL_DRAIN="require('$PROJECT_DIR/server/lib/capture/spool-processor').processSpool('$PROJECT_DIR')"
|
|
48
|
+
fi
|
|
49
|
+
[ -n "$SPOOL_DRAIN" ] && node -e "$SPOOL_DRAIN" 2>/dev/null
|
|
31
50
|
else
|
|
32
51
|
PLIST="$HOME/Library/LaunchAgents/com.tlc.server.plist"
|
|
33
|
-
|
|
34
52
|
if [ -f "$PLIST" ]; then
|
|
35
53
|
launchctl kickstart -k "gui/$(id -u)/com.tlc.server" 2>/dev/null
|
|
36
54
|
elif [ -f "$PROJECT_DIR/server/index.js" ]; then
|
|
37
55
|
nohup node "$PROJECT_DIR/server/index.js" > "$HOME/.tlc/logs/server.log" 2>&1 &
|
|
38
56
|
fi
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
sleep 2
|
|
58
|
+
if curl -sf --max-time 1 "http://localhost:${TLC_PORT}/api/health" > /dev/null 2>&1; then
|
|
59
|
+
echo " TLC Server: ✅ started (:${TLC_PORT})"
|
|
60
|
+
else
|
|
61
|
+
echo " TLC Server: ❌ failed to start"
|
|
62
|
+
ERRORS=$((ERRORS + 1))
|
|
63
|
+
fi
|
|
44
64
|
fi
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
echo "
|
|
66
|
+
SPOOL_FILE="$PROJECT_DIR/.tlc/memory/.spool.jsonl"
|
|
67
|
+
SPOOL_COUNT=0
|
|
68
|
+
if [ -f "$SPOOL_FILE" ]; then
|
|
69
|
+
SPOOL_COUNT=$(wc -l < "$SPOOL_FILE" 2>/dev/null || echo "0")
|
|
70
|
+
fi
|
|
71
|
+
SPOOL_COUNT=$(echo "$SPOOL_COUNT" | tr -d ' ')
|
|
72
|
+
if [ "$SPOOL_COUNT" -gt 0 ] 2>/dev/null; then
|
|
73
|
+
echo " Memory Spool: ⚠️ ${SPOOL_COUNT} entries pending drain"
|
|
74
|
+
WARNINGS=$((WARNINGS + 1))
|
|
54
75
|
else
|
|
55
|
-
|
|
56
|
-
if command -v tlc-core >/dev/null 2>&1; then
|
|
57
|
-
echo "tlc-core orchestrator: not running — starting..."
|
|
58
|
-
tlc-core start > /dev/null 2>&1 &
|
|
59
|
-
# Wait up to 15s for health
|
|
60
|
-
for i in 1 2 3 4 5; do
|
|
61
|
-
sleep 3
|
|
62
|
-
if curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
63
|
-
echo "tlc-core orchestrator: started successfully"
|
|
64
|
-
break
|
|
65
|
-
fi
|
|
66
|
-
done
|
|
67
|
-
if ! curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
68
|
-
echo "tlc-core orchestrator: failed to start. Run 'tlc-core start' manually."
|
|
69
|
-
fi
|
|
70
|
-
else
|
|
71
|
-
echo "tlc-core orchestrator: not installed. Run: git clone git@github.com:TwentyTwoLabs22/tlc-core.git && cd cli && npm install && npm link"
|
|
72
|
-
fi
|
|
76
|
+
echo " Memory: ✅ ready (spool empty)"
|
|
73
77
|
fi
|
|
74
78
|
|
|
75
|
-
# ─── CodeDB
|
|
79
|
+
# ─── 3. CodeDB ───────────────────────────────────────────
|
|
76
80
|
if command -v codedb >/dev/null 2>&1; then
|
|
77
81
|
if curl -sf --max-time 1 "http://localhost:7719/health" > /dev/null 2>&1; then
|
|
78
|
-
|
|
82
|
+
CODEDB_FILES=$(curl -sf --max-time 1 "http://localhost:7719/status" 2>/dev/null | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.files||'?')}catch{console.log('?')}" 2>/dev/null || echo "?")
|
|
83
|
+
echo " CodeDB: ✅ running (${CODEDB_FILES} files indexed)"
|
|
79
84
|
else
|
|
80
85
|
codedb serve "$PROJECT_DIR" > /dev/null 2>&1 &
|
|
81
|
-
echo "
|
|
86
|
+
echo " CodeDB: ✅ started indexing"
|
|
82
87
|
fi
|
|
83
|
-
# Ensure .mcp.json
|
|
88
|
+
# Ensure .mcp.json
|
|
84
89
|
if [ ! -f "$PROJECT_DIR/.mcp.json" ]; then
|
|
85
90
|
cat > "$PROJECT_DIR/.mcp.json" <<'MCPEOF'
|
|
86
91
|
{
|
|
@@ -92,126 +97,210 @@ if command -v codedb >/dev/null 2>&1; then
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
MCPEOF
|
|
95
|
-
echo "
|
|
100
|
+
echo " CodeDB MCP: ✅ created .mcp.json"
|
|
96
101
|
fi
|
|
102
|
+
else
|
|
103
|
+
echo " CodeDB: ❌ not installed (run: npx tlc-claude-code)"
|
|
104
|
+
ERRORS=$((ERRORS + 1))
|
|
97
105
|
fi
|
|
98
106
|
|
|
99
|
-
# ───
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
# ─── 4. tlc-core Orchestrator + Containers ────────────────
|
|
108
|
+
if [ "${TLC_CORE_AUTOSTART}" = "false" ]; then
|
|
109
|
+
echo " Orchestrator: ⏸ disabled (TLC_CORE_AUTOSTART=false)"
|
|
110
|
+
elif curl -sf --max-time 2 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
111
|
+
ORCH_HEALTH=$(curl -sf --max-time 2 "http://localhost:${TLC_CORE_PORT}/health" 2>/dev/null)
|
|
112
|
+
PG_STATUS=$(echo "$ORCH_HEALTH" | node -e "try{console.log(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).postgres)}catch{console.log('?')}" 2>/dev/null)
|
|
113
|
+
DOCKER_STATUS=$(echo "$ORCH_HEALTH" | node -e "try{console.log(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).docker)}catch{console.log('?')}" 2>/dev/null)
|
|
114
|
+
RUNNING_SESSIONS=$(curl -sf --max-time 2 "http://localhost:${TLC_CORE_PORT}/sessions" 2>/dev/null | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));const r=d.filter(s=>s.status==='running');console.log(r.length+'/'+d.length)}catch{console.log('?')}" 2>/dev/null)
|
|
115
|
+
echo " Orchestrator: ✅ healthy (pg:${PG_STATUS}, docker:${DOCKER_STATUS}, sessions:${RUNNING_SESSIONS})"
|
|
116
|
+
|
|
117
|
+
# Check agent-runner container
|
|
118
|
+
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "tlc-agent-runner"; then
|
|
119
|
+
TMUX_COUNT=$(docker exec tlc-agent-runner tmux list-sessions 2>/dev/null | wc -l | tr -d ' ')
|
|
120
|
+
echo " Agent Runner: ✅ running (${TMUX_COUNT} tmux sessions)"
|
|
121
|
+
else
|
|
122
|
+
echo " Agent Runner: ❌ container not running"
|
|
123
|
+
ERRORS=$((ERRORS + 1))
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Check session monitor + watchdog
|
|
127
|
+
ORCH_LOGS=$(docker logs tlc-core-orchestrator-1 --tail 50 2>&1)
|
|
128
|
+
if echo "$ORCH_LOGS" | grep -q "Session monitor started"; then
|
|
129
|
+
echo " Session Monitor: ✅ active (10s interval)"
|
|
130
|
+
else
|
|
131
|
+
echo " Session Monitor: ❌ not running — orchestrator needs restart"
|
|
132
|
+
ERRORS=$((ERRORS + 1))
|
|
133
|
+
fi
|
|
134
|
+
if echo "$ORCH_LOGS" | grep -q "Watchdog started\|watchdog.*Started"; then
|
|
135
|
+
echo " Watchdog: ✅ active (60s interval)"
|
|
136
|
+
else
|
|
137
|
+
echo " Watchdog: ⚠️ not detected in logs"
|
|
138
|
+
WARNINGS=$((WARNINGS + 1))
|
|
139
|
+
fi
|
|
140
|
+
else
|
|
141
|
+
if command -v tlc-core >/dev/null 2>&1; then
|
|
142
|
+
echo " Orchestrator: ❌ not running — starting..."
|
|
143
|
+
tlc-core start > /dev/null 2>&1 &
|
|
144
|
+
for i in 1 2 3 4 5; do
|
|
145
|
+
sleep 3
|
|
146
|
+
curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1 && break
|
|
147
|
+
done
|
|
148
|
+
if curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
149
|
+
echo " Orchestrator: ✅ started"
|
|
150
|
+
else
|
|
151
|
+
echo " Orchestrator: ❌ failed to start. Run 'tlc-core start' manually."
|
|
152
|
+
ERRORS=$((ERRORS + 1))
|
|
153
|
+
fi
|
|
154
|
+
else
|
|
155
|
+
echo " Orchestrator: ❌ not installed"
|
|
156
|
+
ERRORS=$((ERRORS + 1))
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ─── 5. LLM CLI Versions ─────────────────────────────────
|
|
161
|
+
CLAUDE_VER=$(claude --version 2>/dev/null | head -1 || echo "not installed")
|
|
162
|
+
CODEX_VER=$(codex --version 2>/dev/null | head -1 || echo "not installed")
|
|
163
|
+
GEMINI_VER=$(gemini --version 2>/dev/null | head -1 || echo "not installed")
|
|
164
|
+
|
|
165
|
+
PROVIDER_COUNT=0
|
|
166
|
+
PROVIDER_LIST=""
|
|
167
|
+
if [ "$CLAUDE_VER" != "not installed" ]; then
|
|
168
|
+
PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
|
|
169
|
+
PROVIDER_LIST="${PROVIDER_LIST}claude(${CLAUDE_VER}), "
|
|
170
|
+
fi
|
|
171
|
+
if [ "$CODEX_VER" != "not installed" ]; then
|
|
172
|
+
PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
|
|
173
|
+
PROVIDER_LIST="${PROVIDER_LIST}codex(${CODEX_VER}), "
|
|
111
174
|
fi
|
|
112
|
-
if [
|
|
113
|
-
|
|
175
|
+
if [ "$GEMINI_VER" != "not installed" ]; then
|
|
176
|
+
PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
|
|
177
|
+
PROVIDER_LIST="${PROVIDER_LIST}gemini(${GEMINI_VER}), "
|
|
114
178
|
fi
|
|
179
|
+
PROVIDER_LIST=$(echo "$PROVIDER_LIST" | sed 's/, $//')
|
|
115
180
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
181
|
+
if [ "$PROVIDER_COUNT" -gt 0 ]; then
|
|
182
|
+
echo " LLM Providers: ✅ ${PROVIDER_COUNT} available — ${PROVIDER_LIST}"
|
|
183
|
+
else
|
|
184
|
+
echo " LLM Providers: ❌ none found"
|
|
185
|
+
ERRORS=$((ERRORS + 1))
|
|
186
|
+
fi
|
|
121
187
|
|
|
188
|
+
# Write router state
|
|
122
189
|
STATE_DIR="$PROJECT_DIR/.tlc"
|
|
123
190
|
STATE_FILE="$STATE_DIR/.router-state.json"
|
|
124
191
|
mkdir -p "$STATE_DIR"
|
|
125
192
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if [ "$STATE_AGE" -lt 3600 ]; then
|
|
131
|
-
STALE=false
|
|
132
|
-
fi
|
|
133
|
-
fi
|
|
134
|
-
|
|
135
|
-
if [ "$STALE" = true ]; then
|
|
136
|
-
# Probe each provider
|
|
137
|
-
CLAUDE_PATH=$(command -v claude 2>/dev/null || echo "")
|
|
138
|
-
CODEX_PATH=$(command -v codex 2>/dev/null || echo "")
|
|
139
|
-
GEMINI_PATH=$(command -v gemini 2>/dev/null || echo "")
|
|
140
|
-
|
|
141
|
-
CLAUDE_OK="false"
|
|
142
|
-
CODEX_OK="false"
|
|
143
|
-
GEMINI_OK="false"
|
|
144
|
-
|
|
145
|
-
[ -n "$CLAUDE_PATH" ] && CLAUDE_OK="true"
|
|
146
|
-
[ -n "$CODEX_PATH" ] && CODEX_OK="true"
|
|
147
|
-
[ -n "$GEMINI_PATH" ] && GEMINI_OK="true"
|
|
148
|
-
|
|
149
|
-
# Count available
|
|
150
|
-
AVAILABLE=0
|
|
151
|
-
[ "$CLAUDE_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
152
|
-
[ "$CODEX_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
153
|
-
[ "$GEMINI_OK" = "true" ] && AVAILABLE=$((AVAILABLE + 1))
|
|
154
|
-
|
|
155
|
-
# Read configured providers from .tlc.json
|
|
156
|
-
CONFIGURED_PROVIDERS=""
|
|
157
|
-
if command -v jq >/dev/null 2>&1; then
|
|
158
|
-
CONFIGURED_PROVIDERS=$(jq -r '.router.providers // {} | keys[]' .tlc.json 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
|
159
|
-
fi
|
|
193
|
+
CLAUDE_OK="false"; CODEX_OK="false"; GEMINI_OK="false"
|
|
194
|
+
[ "$CLAUDE_VER" != "not installed" ] && CLAUDE_OK="true"
|
|
195
|
+
[ "$CODEX_VER" != "not installed" ] && CODEX_OK="true"
|
|
196
|
+
[ "$GEMINI_VER" != "not installed" ] && GEMINI_OK="true"
|
|
160
197
|
|
|
161
|
-
|
|
162
|
-
cat > "$STATE_FILE" <<STATEEOF
|
|
198
|
+
cat > "$STATE_FILE" <<STATEEOF
|
|
163
199
|
{
|
|
164
200
|
"probed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
165
201
|
"ttl_seconds": 3600,
|
|
166
202
|
"providers": {
|
|
167
|
-
"claude": {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
},
|
|
171
|
-
"codex": {
|
|
172
|
-
"available": $CODEX_OK,
|
|
173
|
-
"path": "$CODEX_PATH"
|
|
174
|
-
},
|
|
175
|
-
"gemini": {
|
|
176
|
-
"available": $GEMINI_OK,
|
|
177
|
-
"path": "$GEMINI_PATH"
|
|
178
|
-
}
|
|
203
|
+
"claude": { "available": $CLAUDE_OK, "version": "$CLAUDE_VER" },
|
|
204
|
+
"codex": { "available": $CODEX_OK, "version": "$CODEX_VER" },
|
|
205
|
+
"gemini": { "available": $GEMINI_OK, "version": "$GEMINI_VER" }
|
|
179
206
|
},
|
|
180
|
-
"summary": {
|
|
181
|
-
"available_count": $AVAILABLE,
|
|
182
|
-
"configured": "$CONFIGURED_PROVIDERS"
|
|
183
|
-
}
|
|
207
|
+
"summary": { "available_count": $PROVIDER_COUNT }
|
|
184
208
|
}
|
|
185
209
|
STATEEOF
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
# ─── 6. Git + Remote Sync ────────────────────────────────
|
|
212
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
213
|
+
BEHIND=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "?")
|
|
214
|
+
AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "?")
|
|
215
|
+
|
|
216
|
+
if [ "$BEHIND" = "0" ] && [ "$AHEAD" = "0" ]; then
|
|
217
|
+
echo " Git (${CURRENT_BRANCH}): ✅ in sync with remote"
|
|
218
|
+
elif [ "$BEHIND" != "?" ] && [ "$BEHIND" -gt 0 ] 2>/dev/null; then
|
|
219
|
+
echo " Git (${CURRENT_BRANCH}): ⚠️ ${BEHIND} commits behind remote — run: git pull"
|
|
220
|
+
WARNINGS=$((WARNINGS + 1))
|
|
221
|
+
elif [ "$AHEAD" != "?" ] && [ "$AHEAD" -gt 0 ] 2>/dev/null; then
|
|
222
|
+
echo " Git (${CURRENT_BRANCH}): ⚠️ ${AHEAD} commits ahead — push pending"
|
|
223
|
+
WARNINGS=$((WARNINGS + 1))
|
|
198
224
|
else
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
225
|
+
echo " Git (${CURRENT_BRANCH}): ⚠️ remote tracking unknown"
|
|
226
|
+
WARNINGS=$((WARNINGS + 1))
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# ─── 7. Workspace Awareness ──────────────────────────────
|
|
230
|
+
WORKSPACE_FILE=""
|
|
231
|
+
CHECK_DIR="$PROJECT_DIR"
|
|
232
|
+
while [ "$CHECK_DIR" != "/" ]; do
|
|
233
|
+
if [ -f "$CHECK_DIR/.tlc-workspace.json" ]; then
|
|
234
|
+
WORKSPACE_FILE="$CHECK_DIR/.tlc-workspace.json"
|
|
235
|
+
break
|
|
203
236
|
fi
|
|
237
|
+
CHECK_DIR=$(dirname "$CHECK_DIR")
|
|
238
|
+
done
|
|
239
|
+
|
|
240
|
+
if [ -n "$WORKSPACE_FILE" ]; then
|
|
241
|
+
WORKSPACE_NAME=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$WORKSPACE_FILE','utf8')).workspace)}catch{console.log('?')}" 2>/dev/null)
|
|
242
|
+
REPO_COUNT=$(node -e "try{console.log(Object.keys(JSON.parse(require('fs').readFileSync('$WORKSPACE_FILE','utf8')).repos).length)}catch{console.log('?')}" 2>/dev/null)
|
|
243
|
+
echo " Workspace: ✅ ${WORKSPACE_NAME} (${REPO_COUNT} repos)"
|
|
244
|
+
|
|
245
|
+
# Check each repo's sync status
|
|
246
|
+
WORKSPACE_ROOT=$(dirname "$WORKSPACE_FILE")
|
|
247
|
+
node -e "
|
|
248
|
+
const fs = require('fs');
|
|
249
|
+
const path = require('path');
|
|
250
|
+
const { execSync } = require('child_process');
|
|
251
|
+
const wsRoot = process.argv[1];
|
|
252
|
+
const ws = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
253
|
+
for (const [name, repo] of Object.entries(ws.repos)) {
|
|
254
|
+
const rp = path.resolve(wsRoot, repo.path);
|
|
255
|
+
try {
|
|
256
|
+
fs.accessSync(rp);
|
|
257
|
+
const branch = execSync('git -C ' + JSON.stringify(rp) + ' branch --show-current', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
258
|
+
let status = 'unknown';
|
|
259
|
+
try {
|
|
260
|
+
const behind = execSync('git -C ' + JSON.stringify(rp) + ' rev-list --count HEAD..@{u}', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
261
|
+
status = behind === '0' ? 'in sync' : behind + ' behind';
|
|
262
|
+
} catch { status = 'no remote tracking'; }
|
|
263
|
+
console.log(' [' + repo.prefix + '] ' + branch + ': ' + status);
|
|
264
|
+
} catch {
|
|
265
|
+
console.log(' [' + repo.prefix + '] not cloned');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
" "$WORKSPACE_ROOT" "$WORKSPACE_FILE" 2>/dev/null
|
|
269
|
+
else
|
|
270
|
+
echo " Workspace: ⚠️ no .tlc-workspace.json found (single-repo mode)"
|
|
271
|
+
WARNINGS=$((WARNINGS + 1))
|
|
204
272
|
fi
|
|
205
273
|
|
|
206
|
-
# ───
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
274
|
+
# ─── 8. CI/CD Status ─────────────────────────────────────
|
|
275
|
+
if command -v gh >/dev/null 2>&1; then
|
|
276
|
+
LAST_RUN=$(gh run list --limit 1 --json conclusion,status,name -q '.[0]' 2>/dev/null)
|
|
277
|
+
if [ -n "$LAST_RUN" ]; then
|
|
278
|
+
RUN_STATUS=$(echo "$LAST_RUN" | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.conclusion||d.status)}catch{console.log('?')}" 2>/dev/null)
|
|
279
|
+
RUN_NAME=$(echo "$LAST_RUN" | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.name)}catch{console.log('?')}" 2>/dev/null)
|
|
280
|
+
if [ "$RUN_STATUS" = "success" ]; then
|
|
281
|
+
echo " CI/CD: ✅ last run passed (${RUN_NAME})"
|
|
282
|
+
elif [ "$RUN_STATUS" = "failure" ]; then
|
|
283
|
+
echo " CI/CD: ❌ last run FAILED (${RUN_NAME})"
|
|
284
|
+
ERRORS=$((ERRORS + 1))
|
|
285
|
+
else
|
|
286
|
+
echo " CI/CD: ⏳ ${RUN_STATUS} (${RUN_NAME})"
|
|
287
|
+
fi
|
|
288
|
+
else
|
|
289
|
+
echo " CI/CD: ⚠️ no recent runs"
|
|
290
|
+
fi
|
|
291
|
+
else
|
|
292
|
+
echo " CI/CD: ⚠️ gh CLI not installed"
|
|
293
|
+
WARNINGS=$((WARNINGS + 1))
|
|
212
294
|
fi
|
|
213
|
-
|
|
214
|
-
|
|
295
|
+
|
|
296
|
+
# ─── Summary ─────────────────────────────────────────────
|
|
297
|
+
echo "────────────────────────────────────────────────────"
|
|
298
|
+
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
|
299
|
+
echo "All systems healthy. ✅"
|
|
300
|
+
elif [ "$ERRORS" -eq 0 ]; then
|
|
301
|
+
echo "${WARNINGS} warning(s), 0 errors. Systems operational."
|
|
302
|
+
else
|
|
303
|
+
echo "⚠️ ${ERRORS} error(s), ${WARNINGS} warning(s). Check above."
|
|
215
304
|
fi
|
|
216
305
|
|
|
217
306
|
exit 0
|