@codefilabs/tq 0.0.2 → 0.1.0
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/README.md +100 -3
- package/package.json +4 -1
- package/scripts/tq +126 -17
- package/scripts/tq-converse +726 -0
- package/scripts/tq-cron-sync +92 -0
- package/scripts/tq-install.sh +23 -9
- package/scripts/tq-message +96 -27
- package/scripts/tq-setup +1 -1
- package/scripts/tq-telegram-poll +167 -15
- package/skills/tq/SKILL.md +42 -6
- package/skills/tq/references/session-naming.md +16 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tq — Task Queue for Claude
|
|
2
2
|
|
|
3
|
-
A lightweight task queue runner that spawns Claude AI tasks as independent tmux sessions.
|
|
3
|
+
A lightweight task queue runner and conversation manager that spawns Claude AI tasks as independent tmux sessions. Supports both one-off batch queues and persistent interactive conversations via Telegram.
|
|
4
4
|
|
|
5
5
|
## What It Does
|
|
6
6
|
|
|
@@ -31,9 +31,9 @@ Here's exactly what that command does:
|
|
|
31
31
|
|
|
32
32
|
1. **Downloads the install script** from this repo over HTTPS (`-fsSL` = fail on error, silent, follow redirects) and pipes it directly to `bash` — nothing is saved to disk.
|
|
33
33
|
|
|
34
|
-
2. **Registers the
|
|
34
|
+
2. **Registers the codefilabs marketplace** with Claude Code by running `claude plugin marketplace add codefilabs/marketplace`. This clones the marketplace repo into `~/.claude/plugins/marketplaces/codefilabs/` so Claude knows where to find it.
|
|
35
35
|
|
|
36
|
-
3. **Installs the tq plugin** by running `claude plugin install tq@
|
|
36
|
+
3. **Installs the tq plugin** by running `claude plugin install tq@codefilabs`. This caches the plugin files into `~/.claude/plugins/cache/tq/codefilabs/<version>/` and registers the plugin's skills and slash commands with Claude Code. After this, Claude will recognize commands like `/todo`, `/schedule`, `/jobs`, and `/health`.
|
|
37
37
|
|
|
38
38
|
4. **Symlinks `tq`** into `/opt/homebrew/bin` (Apple Silicon) or `/usr/local/bin` (Intel Mac) so the command is available in your shell and in cron jobs.
|
|
39
39
|
|
|
@@ -216,6 +216,8 @@ Once installed, Claude can manage your task queues via slash commands:
|
|
|
216
216
|
| `/health` | System-wide diagnostics |
|
|
217
217
|
| `/setup-telegram` | Interactive wizard to configure Telegram notifications |
|
|
218
218
|
| `/install` | Symlink tq binaries to PATH |
|
|
219
|
+
| `/converse [start\|stop\|status]` | Manage Telegram conversation sessions |
|
|
220
|
+
| `/tq-reply` | Send a response back to Telegram (used by Claude in conversation mode) |
|
|
219
221
|
|
|
220
222
|
Claude will infer the queue name from context: "every morning" → `morning.yaml`, "daily" → `daily.yaml`, or the current directory's basename if no schedule keyword is present.
|
|
221
223
|
|
|
@@ -294,3 +296,98 @@ crontab -e
|
|
|
294
296
|
Logs accumulate in `~/.tq/logs/tq.log`.
|
|
295
297
|
|
|
296
298
|
See `skills/tq/references/cron-expressions.md` for a natural language → cron expression reference.
|
|
299
|
+
|
|
300
|
+
## Conversation Mode (Telegram)
|
|
301
|
+
|
|
302
|
+
Conversation mode enables interactive, back-and-forth conversations with Claude Code via Telegram. An orchestrator Claude session routes your messages to the appropriate conversation, automatically creating new sessions for new topics and resuming existing ones.
|
|
303
|
+
|
|
304
|
+
### Setup
|
|
305
|
+
|
|
306
|
+
1. Configure Telegram: run `/setup-telegram` in Claude Code (or `tq-setup`)
|
|
307
|
+
2. Add the polling cron job:
|
|
308
|
+
```bash
|
|
309
|
+
* * * * * /opt/homebrew/bin/tq-telegram-poll >> ~/.tq/logs/tq-telegram.log 2>&1
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Starting Conversations
|
|
313
|
+
|
|
314
|
+
Send `/converse` from Telegram (or run `tq-converse start` from CLI). This launches the orchestrator.
|
|
315
|
+
|
|
316
|
+
Once the orchestrator is running, just send messages normally. The orchestrator will:
|
|
317
|
+
- **New topic** → create a new conversation session with a descriptive slug (e.g., `fix-auth-bug`)
|
|
318
|
+
- **Related to existing** → route to the appropriate session
|
|
319
|
+
- **Telegram reply** → automatically route to the conversation that sent the original message
|
|
320
|
+
- **Explicit routing** → prefix with `#slug-name` to target a specific session
|
|
321
|
+
|
|
322
|
+
### Example Flow
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
You: "fix the login bug in the auth module"
|
|
326
|
+
tq: [new: fix-auth] Started conversation: Fix login bug in auth module
|
|
327
|
+
|
|
328
|
+
You: "what did you find?"
|
|
329
|
+
tq: [fix-auth] Found 3 issues in src/auth.py: ...
|
|
330
|
+
|
|
331
|
+
You: "refactor the payment service to use Stripe v2"
|
|
332
|
+
tq: [new: refactor-payments] Started conversation: Refactor payment service for Stripe v2
|
|
333
|
+
|
|
334
|
+
You: #fix-auth "also check the password reset flow"
|
|
335
|
+
tq: [fix-auth] Checking password reset flow...
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Telegram Commands
|
|
339
|
+
|
|
340
|
+
| Command | Purpose |
|
|
341
|
+
|---------|---------|
|
|
342
|
+
| `/converse` | Start the orchestrator |
|
|
343
|
+
| `/stop` | Stop the orchestrator |
|
|
344
|
+
| `/stop <slug>` | Stop a specific conversation |
|
|
345
|
+
| `/status` | Show all sessions |
|
|
346
|
+
| `/list` | List active conversations |
|
|
347
|
+
|
|
348
|
+
### How It Works
|
|
349
|
+
|
|
350
|
+
1. **tq-telegram-poll** runs every minute via cron, fetches new Telegram messages
|
|
351
|
+
2. **3-tier routing** determines where each message goes:
|
|
352
|
+
- Tier 1: Telegram reply → deterministic lookup via registry message ID mapping
|
|
353
|
+
- Tier 2: `#slug` prefix → route directly to named session
|
|
354
|
+
- Tier 3: Send to orchestrator Claude for smart routing
|
|
355
|
+
3. **Orchestrator** (a persistent Claude Code session) reads the conversation registry, decides whether to route to an existing session or spawn a new one
|
|
356
|
+
4. **Child sessions** (each a persistent Claude Code interactive session) process messages and respond via `/tq-reply`
|
|
357
|
+
5. **Responses** are sent back to Telegram as threaded replies, with the session slug as a label
|
|
358
|
+
|
|
359
|
+
### State
|
|
360
|
+
|
|
361
|
+
Conversation state lives in `~/.tq/conversations/`:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
~/.tq/conversations/
|
|
365
|
+
├── registry.json ← session registry (slugs, descriptions, message IDs)
|
|
366
|
+
├── orchestrator/ ← orchestrator Claude settings and instructions
|
|
367
|
+
│ ├── .tq-orchestrator.md
|
|
368
|
+
│ ├── settings.json
|
|
369
|
+
│ └── hooks/
|
|
370
|
+
└── sessions/
|
|
371
|
+
├── fix-auth/ ← per-session state
|
|
372
|
+
│ ├── .tq-converse.md
|
|
373
|
+
│ ├── settings.json
|
|
374
|
+
│ ├── current-slug
|
|
375
|
+
│ ├── reply-to-msg-id
|
|
376
|
+
│ ├── inbox/ ← received messages (timestamped)
|
|
377
|
+
│ └── outbox/ ← sent responses (timestamped)
|
|
378
|
+
└── refactor-payments/
|
|
379
|
+
└── ...
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### CLI Commands
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
tq-converse start # start orchestrator
|
|
386
|
+
tq-converse spawn <slug> [opts] # create a child session
|
|
387
|
+
tq-converse route <slug> <message> # send to a session
|
|
388
|
+
tq-converse send <message> # send to orchestrator
|
|
389
|
+
tq-converse list # list active sessions
|
|
390
|
+
tq-converse status # show all session details
|
|
391
|
+
tq-converse stop [<slug>] # stop session or orchestrator
|
|
392
|
+
tq-converse registry # dump the session registry
|
|
393
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codefilabs/tq",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A lightweight task queue runner that spawns Claude AI tasks as independent tmux sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -37,5 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"postpublish": "node tools/update-marketplace.js"
|
|
40
43
|
}
|
|
41
44
|
}
|
package/scripts/tq
CHANGED
|
@@ -10,6 +10,7 @@ PROMPT_TEXT=""
|
|
|
10
10
|
TASK_NAME="adhoc"
|
|
11
11
|
TASK_CWD=""
|
|
12
12
|
NOTIFY=""
|
|
13
|
+
CHROME=1
|
|
13
14
|
|
|
14
15
|
while [[ $# -gt 0 ]]; do
|
|
15
16
|
case "${1:-}" in
|
|
@@ -18,6 +19,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
18
19
|
--name) TASK_NAME="${2:-}"; shift 2 ;;
|
|
19
20
|
--cwd) TASK_CWD="${2:-}"; shift 2 ;;
|
|
20
21
|
--notify) NOTIFY="${2:-}"; shift 2 ;;
|
|
22
|
+
--no-chrome) CHROME=0; shift ;;
|
|
21
23
|
--) shift; break ;;
|
|
22
24
|
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
23
25
|
*) break ;;
|
|
@@ -92,6 +94,7 @@ if [[ "$STATUS_MODE" == "1" ]]; then
|
|
|
92
94
|
if [[ "$STATUS" == "running" ]]; then
|
|
93
95
|
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
94
96
|
sed -i '' 's/^status=running/status=done/' "$STATE_FILE"
|
|
97
|
+
echo "completed=$(date +%s)" >> "$STATE_FILE"
|
|
95
98
|
STATUS="done"
|
|
96
99
|
fi
|
|
97
100
|
fi
|
|
@@ -125,7 +128,7 @@ fi
|
|
|
125
128
|
# (Using a temp file avoids bash 3.2 bug: single quotes inside <<'HEREDOC'
|
|
126
129
|
# inside $() are incorrectly scanned by the quote counter.)
|
|
127
130
|
# ---------------------------------------------------------------------------
|
|
128
|
-
PARSE_SCRIPT=$(mktemp /tmp/tq-parse-XXXXXX
|
|
131
|
+
PARSE_SCRIPT=$(mktemp /tmp/tq-parse-XXXXXX)
|
|
129
132
|
trap 'rm -f "$PARSE_SCRIPT"' EXIT
|
|
130
133
|
|
|
131
134
|
cat > "$PARSE_SCRIPT" <<'PYEOF'
|
|
@@ -136,7 +139,7 @@ os.makedirs(sessions_dir, exist_ok=True)
|
|
|
136
139
|
|
|
137
140
|
# Support direct prompt mode: --prompt <text> <state_dir> [cwd]
|
|
138
141
|
if len(sys.argv) > 1 and sys.argv[1] == '--prompt':
|
|
139
|
-
tasks = [(sys.argv[2].strip(), sys.argv[4] if len(sys.argv) > 4 else '')]
|
|
142
|
+
tasks = [(sys.argv[2].strip(), sys.argv[4] if len(sys.argv) > 4 else '', '', '')]
|
|
140
143
|
state_dir = sys.argv[3]
|
|
141
144
|
else:
|
|
142
145
|
queue_file = sys.argv[1]
|
|
@@ -155,11 +158,62 @@ else:
|
|
|
155
158
|
cwd = m.group(1).strip().strip('"\'')
|
|
156
159
|
break
|
|
157
160
|
|
|
161
|
+
# Extract top-level reset policy
|
|
162
|
+
reset_mode = ''
|
|
163
|
+
for line in lines:
|
|
164
|
+
m = re.match(r'^reset:\s*(.+)$', line)
|
|
165
|
+
if m:
|
|
166
|
+
reset_mode = m.group(1).strip().strip('"\'')
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
# Handle queue-level resets before task evaluation.
|
|
170
|
+
# Clears task state files (and .queue-notified) so tasks re-run.
|
|
171
|
+
# daily — reset once per calendar day
|
|
172
|
+
# weekly — reset once per ISO week (Mon–Sun)
|
|
173
|
+
# hourly — reset once per hour
|
|
174
|
+
# always — reset on every tq run
|
|
175
|
+
if reset_mode in ('daily', 'weekly', 'hourly', 'always'):
|
|
176
|
+
import datetime
|
|
177
|
+
now = datetime.datetime.now()
|
|
178
|
+
if reset_mode == 'daily':
|
|
179
|
+
period = now.date().isoformat() # e.g. '2026-03-11'
|
|
180
|
+
elif reset_mode == 'weekly':
|
|
181
|
+
period = now.strftime('%G-W%V') # e.g. '2026-W11'
|
|
182
|
+
elif reset_mode == 'hourly':
|
|
183
|
+
period = now.strftime('%Y-%m-%dT%H') # e.g. '2026-03-11T17'
|
|
184
|
+
else: # always
|
|
185
|
+
period = None
|
|
186
|
+
last_reset_file = os.path.join(state_dir, '.last_reset')
|
|
187
|
+
last_reset = ''
|
|
188
|
+
if period is not None and os.path.exists(last_reset_file):
|
|
189
|
+
with open(last_reset_file) as f:
|
|
190
|
+
last_reset = f.read().strip()
|
|
191
|
+
if period is None or last_reset != period:
|
|
192
|
+
for fname in os.listdir(state_dir):
|
|
193
|
+
fpath = os.path.join(state_dir, fname)
|
|
194
|
+
if not fname.startswith('.') and '.' not in fname:
|
|
195
|
+
os.remove(fpath) # task state file (bare hash)
|
|
196
|
+
elif fname == '.queue-notified':
|
|
197
|
+
os.remove(fpath) # clear so completion fires fresh
|
|
198
|
+
if period is not None:
|
|
199
|
+
with open(last_reset_file, 'w') as f:
|
|
200
|
+
f.write(period)
|
|
201
|
+
|
|
158
202
|
# Parse tasks -- handle inline, block literal (|), block folded (>), quoted
|
|
159
203
|
tasks = []
|
|
204
|
+
current_name = ''
|
|
160
205
|
i = 0
|
|
161
206
|
while i < len(lines):
|
|
162
|
-
|
|
207
|
+
# Reset name at start of each new list item
|
|
208
|
+
if re.match(r'^ - ', lines[i]) and not re.match(r'^ - prompt:', lines[i]):
|
|
209
|
+
current_name = ''
|
|
210
|
+
m_name = re.match(r'^ - name:\s*(.*)', lines[i])
|
|
211
|
+
if m_name:
|
|
212
|
+
current_name = m_name.group(1).strip().strip('"\'')
|
|
213
|
+
i += 1
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
m = re.match(r'^(?: - | )prompt:\s*(.*)', lines[i])
|
|
163
217
|
if not m:
|
|
164
218
|
i += 1
|
|
165
219
|
continue
|
|
@@ -209,9 +263,10 @@ else:
|
|
|
209
263
|
|
|
210
264
|
prompt = prompt.strip()
|
|
211
265
|
if prompt:
|
|
212
|
-
tasks.append((prompt, cwd))
|
|
266
|
+
tasks.append((prompt, cwd, current_name, reset_mode))
|
|
267
|
+
current_name = ''
|
|
213
268
|
|
|
214
|
-
for (prompt, cwd) in tasks:
|
|
269
|
+
for (prompt, cwd, name, reset_mode) in tasks:
|
|
215
270
|
h = hashlib.sha256(prompt.encode()).hexdigest()[:8]
|
|
216
271
|
first_line = prompt.split('\n')[0][:80]
|
|
217
272
|
|
|
@@ -252,8 +307,15 @@ for (prompt, cwd) in tasks:
|
|
|
252
307
|
stop_script += 'set -euo pipefail\n'
|
|
253
308
|
stop_script += '# Mark tq task done\n'
|
|
254
309
|
stop_script += 'STATE_FILE=' + json.dumps(state_file) + '\n'
|
|
310
|
+
stop_script += 'RESET_MODE=' + json.dumps(reset_mode) + '\n'
|
|
255
311
|
stop_script += 'if [[ -f "$STATE_FILE" ]]; then\n'
|
|
256
|
-
stop_script +=
|
|
312
|
+
stop_script += ' if [[ "$RESET_MODE" == "on-complete" ]]; then\n'
|
|
313
|
+
stop_script += ' # on-complete: tq-message runs first, then state is cleared for re-run\n'
|
|
314
|
+
stop_script += ' : # state deletion happens after tq-message below\n'
|
|
315
|
+
stop_script += ' else\n'
|
|
316
|
+
stop_script += " sed -i '' 's/^status=running/status=done/' \"$STATE_FILE\"\n"
|
|
317
|
+
stop_script += ' echo "completed=$(date +%s)" >> "$STATE_FILE"\n'
|
|
318
|
+
stop_script += ' fi\n'
|
|
257
319
|
stop_script += 'fi\n'
|
|
258
320
|
if notify:
|
|
259
321
|
stop_script += '\n# Notification (--notify)\n'
|
|
@@ -283,6 +345,10 @@ for (prompt, cwd) in tasks:
|
|
|
283
345
|
stop_script += ' SESSION="$(grep \'^session=\' "$STATE_FILE" | cut -d= -f2)"\n'
|
|
284
346
|
stop_script += ' tq-message --task "$TQ_HASH" --queue "$TQ_QUEUE_FILE" --state-file "$STATE_FILE" --session "$SESSION"\n'
|
|
285
347
|
stop_script += 'fi\n'
|
|
348
|
+
stop_script += '# on-complete: delete state file so next tq run re-spawns this task\n'
|
|
349
|
+
stop_script += 'if [[ "$RESET_MODE" == "on-complete" && -f "$STATE_FILE" ]]; then\n'
|
|
350
|
+
stop_script += ' rm -f "$STATE_FILE"\n'
|
|
351
|
+
stop_script += 'fi\n'
|
|
286
352
|
with open(stop_hook, 'w') as f:
|
|
287
353
|
f.write(stop_script)
|
|
288
354
|
os.chmod(stop_hook, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
|
@@ -322,23 +388,34 @@ for (prompt, cwd) in tasks:
|
|
|
322
388
|
f.write('os.environ[' + json.dumps(k) + '] = ' + json.dumps(v) + '\n')
|
|
323
389
|
f.write('if cwd:\n')
|
|
324
390
|
f.write(' os.chdir(cwd)\n')
|
|
391
|
+
use_chrome = os.environ.get('TQ_CHROME', '1') == '1'
|
|
392
|
+
|
|
325
393
|
f.write('import shutil, subprocess, time\n')
|
|
326
394
|
f.write('prompt = open(prompt_file).read()\n')
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
395
|
+
if use_chrome:
|
|
396
|
+
f.write('# Open Chrome with Profile 5 (halbotkirchner@gmail.com) before connecting\n')
|
|
397
|
+
f.write('subprocess.Popen(["open", "-a", "Google Chrome", "--args", "--profile-directory=Profile 5"])\n')
|
|
398
|
+
f.write('time.sleep(2)\n')
|
|
399
|
+
|
|
400
|
+
# Build claude args
|
|
401
|
+
claude_args = "['claude', '--settings', settings_file, '--dangerously-skip-permissions'"
|
|
402
|
+
if use_chrome:
|
|
403
|
+
claude_args += ", '--chrome'"
|
|
404
|
+
claude_args += ", prompt]"
|
|
405
|
+
|
|
330
406
|
f.write('# Use reattach-to-user-namespace if available (macOS tmux keychain fix)\n')
|
|
331
407
|
f.write('reattach = shutil.which("reattach-to-user-namespace")\n')
|
|
332
408
|
f.write('if reattach:\n')
|
|
333
|
-
f.write(" os.execvp(reattach, [reattach,
|
|
409
|
+
f.write(" os.execvp(reattach, [reattach, " + claude_args[1:] + ")\n")
|
|
334
410
|
f.write('else:\n')
|
|
335
|
-
f.write(" os.execvp('claude',
|
|
411
|
+
f.write(" os.execvp('claude', " + claude_args + ")\n")
|
|
336
412
|
|
|
337
|
-
print(json.dumps({'hash': h, 'first_line': first_line}))
|
|
413
|
+
print(json.dumps({'hash': h, 'first_line': first_line, 'name': name, 'reset': reset_mode}))
|
|
338
414
|
|
|
339
415
|
PYEOF
|
|
340
416
|
|
|
341
417
|
export TQ_NOTIFY="$NOTIFY"
|
|
418
|
+
export TQ_CHROME="$CHROME"
|
|
342
419
|
|
|
343
420
|
if [[ "$PROMPT_MODE" == "1" ]]; then
|
|
344
421
|
TASK_CWD="${TASK_CWD:-$HOME/.tq/workspace}"
|
|
@@ -363,6 +440,8 @@ fi
|
|
|
363
440
|
while IFS= read -r JSON_LINE; do
|
|
364
441
|
HASH="$(python3 -c "import sys,json; print(json.loads(sys.argv[1])['hash'])" "$JSON_LINE")"
|
|
365
442
|
FIRST_LINE="$(python3 -c "import sys,json; print(json.loads(sys.argv[1])['first_line'])" "$JSON_LINE")"
|
|
443
|
+
TASK_NAME_FIELD="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('name', ''))" "$JSON_LINE")"
|
|
444
|
+
RESET_MODE="$(python3 -c "import sys,json; print(json.loads(sys.argv[1]).get('reset', ''))" "$JSON_LINE")"
|
|
366
445
|
STATE_FILE="$STATE_DIR/$HASH"
|
|
367
446
|
LAUNCHER="$STATE_DIR/$HASH.launch.py"
|
|
368
447
|
|
|
@@ -370,8 +449,33 @@ while IFS= read -r JSON_LINE; do
|
|
|
370
449
|
if [[ -f "$STATE_FILE" ]]; then
|
|
371
450
|
STATUS="$(grep '^status=' "$STATE_FILE" | cut -d= -f2)"
|
|
372
451
|
if [[ "$STATUS" == "done" ]]; then
|
|
373
|
-
|
|
374
|
-
|
|
452
|
+
if [[ -n "$RESET_MODE" && "$RESET_MODE" != "on-complete" ]]; then
|
|
453
|
+
COMPLETED="$(grep '^completed=' "$STATE_FILE" | cut -d= -f2)"
|
|
454
|
+
NOW="$(date +%s)"
|
|
455
|
+
TTL_SECONDS="$(python3 -c "
|
|
456
|
+
import sys
|
|
457
|
+
s = sys.argv[1]
|
|
458
|
+
if s.endswith('h'):
|
|
459
|
+
print(int(s[:-1]) * 3600)
|
|
460
|
+
elif s.endswith('d'):
|
|
461
|
+
print(int(s[:-1]) * 86400)
|
|
462
|
+
elif s.endswith('m'):
|
|
463
|
+
print(int(s[:-1]) * 60)
|
|
464
|
+
else:
|
|
465
|
+
print(0)
|
|
466
|
+
" "$RESET_MODE")"
|
|
467
|
+
if [[ -n "$COMPLETED" && "$TTL_SECONDS" -gt 0 && $(( NOW - COMPLETED )) -gt "$TTL_SECONDS" ]]; then
|
|
468
|
+
rm -f "$STATE_FILE"
|
|
469
|
+
echo " [reset] $FIRST_LINE (TTL expired)"
|
|
470
|
+
# Fall through to spawn logic below
|
|
471
|
+
else
|
|
472
|
+
echo " [done] $FIRST_LINE"
|
|
473
|
+
continue
|
|
474
|
+
fi
|
|
475
|
+
else
|
|
476
|
+
echo " [done] $FIRST_LINE"
|
|
477
|
+
continue
|
|
478
|
+
fi
|
|
375
479
|
fi
|
|
376
480
|
if [[ "$STATUS" == "running" ]]; then
|
|
377
481
|
SESSION="$(grep '^session=' "$STATE_FILE" | cut -d= -f2)"
|
|
@@ -387,11 +491,16 @@ while IFS= read -r JSON_LINE; do
|
|
|
387
491
|
fi
|
|
388
492
|
fi
|
|
389
493
|
|
|
390
|
-
# Generate session/window names
|
|
494
|
+
# Generate session/window names: prefer YAML name field, fall back to prompt words
|
|
391
495
|
EPOCH_SUFFIX="$(date +%s | tail -c 6)"
|
|
392
|
-
|
|
496
|
+
if [[ -n "$TASK_NAME_FIELD" ]]; then
|
|
497
|
+
SESSION_BASE="$(echo "$TASK_NAME_FIELD" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//' | sed 's/-*$//' | cut -c1-20)"
|
|
498
|
+
WINDOW="$(echo "$TASK_NAME_FIELD" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//' | sed 's/-*$//' | cut -c1-15)"
|
|
499
|
+
else
|
|
500
|
+
SESSION_BASE="$(echo "$FIRST_LINE" | awk '{print $1" "$2" "$3}' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//' | sed 's/-*$//' | cut -c1-20)"
|
|
501
|
+
WINDOW="$(echo "$FIRST_LINE" | awk '{print $1" "$2}' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//' | sed 's/-*$//' | cut -c1-15)"
|
|
502
|
+
fi
|
|
393
503
|
SESSION="tq-${SESSION_BASE}-${EPOCH_SUFFIX}"
|
|
394
|
-
WINDOW="$(echo "$FIRST_LINE" | awk '{print $1" "$2}' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//' | sed 's/-*$//' | cut -c1-15)"
|
|
395
504
|
|
|
396
505
|
# Write state file
|
|
397
506
|
cat > "$STATE_FILE" <<EOF
|