@kitelev/exocortex-cli 15.162.0 → 15.164.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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # ai-task-runner — executed inside spawned tmux session per claimed task.
3
+ # Phase 6: real claude -p --dangerously-skip-permissions invocation.
4
+
5
+ set -euo pipefail
6
+
7
+ TASK_FILE="${1:?task file required}"
8
+ SESSION_LOG="${2:?session log required}"
9
+
10
+ mkdir -p "$(dirname "$SESSION_LOG")"
11
+ exec >>"$SESSION_LOG" 2>&1
12
+
13
+ echo "[runner] $(date '+%Y-%m-%dT%H:%M:%S%z'): start task=$TASK_FILE"
14
+
15
+ # Extract metadata from task frontmatter
16
+ eval "$(/usr/bin/python3 - "$TASK_FILE" <<'PYEOF'
17
+ import sys, re
18
+ fp = sys.argv[1]
19
+ content = open(fp, encoding='utf-8').read()
20
+ end = content.find('\n---', 3)
21
+ fm = content[3:end]
22
+ def get(key):
23
+ m = re.search(rf'^{re.escape(key)}\s*:\s*(.*)$', fm, re.MULTILINE)
24
+ return m.group(1).strip().strip('"') if m else ''
25
+ model = get('aiTask__Task_model') or 'sonnet'
26
+ timeout = get('aiTask__Task_timeoutMinutes') or '30'
27
+ print(f"TASK_MODEL={model}")
28
+ print(f"TASK_TIMEOUT={timeout}")
29
+ PYEOF
30
+ )"
31
+
32
+ # Build prompt: full task file content (frontmatter + body)
33
+ PROMPT="$(< "$TASK_FILE")"
34
+
35
+ echo "[runner] $(date '+%Y-%m-%dT%H:%M:%S%z'): spawning claude model=$TASK_MODEL timeout=${TASK_TIMEOUT}m"
36
+
37
+ # Run real claude -p — exec replaces this process
38
+ exec env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT -u CLAUDE_CODE_EXECPATH \
39
+ claude --dangerously-skip-permissions --model "$TASK_MODEL" -p "$PROMPT"
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # ai-task-worker — scan vault for delegated aiTask, claim atomically, spawn tmux session.
3
+ #
4
+ # Picks tasks where ems__Effort_status=Backlog AND aiTask__Task_delegated="true" AND no claimedBy.
5
+ # Atomic claim via frontmatter update (writes Doing+claimedBy+claimedAt+sessionLog).
6
+ # Spawns a detached tmux session named claude-child-<uuid> executing ai-task-runner,
7
+ # which calls real claude -p --dangerously-skip-permissions with task content as prompt.
8
+
9
+ set -euo pipefail
10
+
11
+ VAULT_DIR="${EXOCORTEX_VAULT:-/Users/kitelev/vault-2025}"
12
+ LOG_DIR="$HOME/.exocortex/logs"
13
+ mkdir -p "$LOG_DIR"
14
+ LOG="$LOG_DIR/aitask-worker.log"
15
+ RUNNER="$HOME/.exocortex/bin/ai-task-runner"
16
+
17
+ log() { echo "[ai-task-worker] $(date '+%Y-%m-%dT%H:%M:%S%z'): $*" | tee -a "$LOG"; }
18
+
19
+ log "Scan start (vault=$VAULT_DIR)"
20
+
21
+ /usr/bin/python3 - "$VAULT_DIR" "$LOG" "$RUNNER" "$$" <<'PYEOF'
22
+ import os, re, sys, datetime, tempfile, shutil, subprocess
23
+
24
+ vault, log_path, runner, parent_pid = sys.argv[1:5]
25
+
26
+ def log(msg):
27
+ ts = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S%z')
28
+ line = f"[ai-task-worker] {ts}: {msg}"
29
+ print(line)
30
+ open(log_path, 'a').write(line + "\n")
31
+
32
+ def now_iso5():
33
+ tz5 = datetime.timezone(datetime.timedelta(hours=5))
34
+ return datetime.datetime.now(tz5).strftime('%Y-%m-%dT%H:%M:%S+0500')
35
+
36
+ def split_fm(text):
37
+ if not text.startswith('---'):
38
+ return None, text
39
+ end = text.find('\n---', 3)
40
+ if end == -1:
41
+ return None, text
42
+ return text[3:end], text[end+4:]
43
+
44
+ def set_field(fm, key, val):
45
+ pat = re.compile(rf'^({re.escape(key)})\s*:.*$', re.MULTILINE)
46
+ if pat.search(fm):
47
+ return pat.sub(f'{key}: {val}', fm, 1)
48
+ return fm.rstrip('\n') + f'\n{key}: {val}\n'
49
+
50
+ def get_field(fm, key):
51
+ m = re.search(rf'^{re.escape(key)}\s*:\s*(.*)$', fm, re.MULTILINE)
52
+ return m.group(1).strip() if m else ''
53
+
54
+ def write_atomic(path, fm, body):
55
+ d = os.path.dirname(path)
56
+ with tempfile.NamedTemporaryFile('w', dir=d, delete=False, suffix='.tmp', encoding='utf-8') as t:
57
+ t.write(f"---{fm}\n---{body}")
58
+ tmp = t.name
59
+ shutil.move(tmp, path)
60
+
61
+ DELEG_PAT = re.compile(r'aiTask__Task_delegated\s*:\s*"?true"?', re.IGNORECASE)
62
+ BACKLOG_PAT = re.compile(r'ems__Effort_status\s*:\s*["\[]*ems__EffortStatusBacklog')
63
+ CLAIMED_PAT = re.compile(r'^aiTask__Task_claimedBy\s*:', re.MULTILINE)
64
+
65
+ candidates = []
66
+ for dp, dns, fns in os.walk(vault):
67
+ dns[:] = [d for d in dns if not d.startswith('.')]
68
+ for fn in fns:
69
+ if not fn.endswith('.md'):
70
+ continue
71
+ fp = os.path.join(dp, fn)
72
+ try:
73
+ content = open(fp, encoding='utf-8').read()
74
+ except Exception:
75
+ continue
76
+ if not content.startswith('---'):
77
+ continue
78
+ fm, _ = split_fm(content)
79
+ if fm is None:
80
+ continue
81
+ if not DELEG_PAT.search(fm):
82
+ continue
83
+ if not BACKLOG_PAT.search(fm):
84
+ continue
85
+ if CLAIMED_PAT.search(fm):
86
+ continue
87
+ candidates.append(fp)
88
+
89
+ log(f"Candidates: {len(candidates)}")
90
+
91
+ for fp in candidates:
92
+ try:
93
+ content = open(fp, encoding='utf-8').read()
94
+ fm, body = split_fm(content)
95
+ if fm is None:
96
+ continue
97
+ uid = get_field(fm, 'exo__Asset_uid').strip('"')
98
+ label = get_field(fm, 'exo__Asset_label').strip('"')
99
+ session_name = f"claude-child-{uid}"
100
+ session_log = f"{os.path.expanduser('~')}/.exocortex/logs/aitask-session-{uid}.log"
101
+ ts = now_iso5()
102
+
103
+ # Atomic claim — set Doing + claimedBy + claimedAt + sessionLog + startTimestamp + updatedAt
104
+ new_fm = fm
105
+ new_fm = set_field(new_fm, 'ems__Effort_status', '"[[ems__EffortStatusDoing]]"')
106
+ new_fm = set_field(new_fm, 'aiTask__Task_claimedBy', f'"{parent_pid}"')
107
+ new_fm = set_field(new_fm, 'aiTask__Task_claimedAt', f'"{ts}"')
108
+ new_fm = set_field(new_fm, 'aiTask__Task_sessionLog', f'"{session_log}"')
109
+ if 'ems__Effort_startTimestamp' not in new_fm:
110
+ new_fm = set_field(new_fm, 'ems__Effort_startTimestamp', ts)
111
+ new_fm = set_field(new_fm, 'exo__Asset_updatedAt', ts)
112
+ write_atomic(fp, new_fm, body)
113
+ log(f"CLAIMED {uid} ({label}) → session={session_name}")
114
+
115
+ # Spawn detached tmux session named claude-child-<full-uuid>
116
+ cmd = ['tmux', 'new-session', '-d', '-s', session_name,
117
+ 'bash', '-l', '-c', f"{runner} '{fp}' '{session_log}' 2>&1 | tee -a '{session_log}'"]
118
+ try:
119
+ subprocess.run(cmd, check=True)
120
+ log(f"SPAWNED tmux session {session_name}")
121
+ except subprocess.CalledProcessError as e:
122
+ log(f"SPAWN_FAIL {uid}: {e}")
123
+ except Exception as e:
124
+ log(f"ERROR {fp}: {e}")
125
+
126
+ log("Scan done")
127
+ PYEOF