taskuary 0.2.0__py3-none-any.whl

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 (62) hide show
  1. taskuary/__init__.py +2 -0
  2. taskuary/agents.py +218 -0
  3. taskuary/artifacts.py +209 -0
  4. taskuary/aws.py +226 -0
  5. taskuary/azure.py +334 -0
  6. taskuary/blackboard.py +150 -0
  7. taskuary/channels.py +768 -0
  8. taskuary/ci.py +237 -0
  9. taskuary/cli.py +47 -0
  10. taskuary/coder.py +132 -0
  11. taskuary/config.py +62 -0
  12. taskuary/db.py +63 -0
  13. taskuary/desktop.py +72 -0
  14. taskuary/devtools.py +352 -0
  15. taskuary/digest.py +104 -0
  16. taskuary/docsync.py +124 -0
  17. taskuary/github.py +136 -0
  18. taskuary/histgen.py +220 -0
  19. taskuary/imapmail.py +150 -0
  20. taskuary/ingest.py +344 -0
  21. taskuary/learn.py +181 -0
  22. taskuary/llm.py +175 -0
  23. taskuary/logs.py +17 -0
  24. taskuary/mcp.py +78 -0
  25. taskuary/messengers.py +205 -0
  26. taskuary/mssql.py +64 -0
  27. taskuary/outbound.py +221 -0
  28. taskuary/phone.py +88 -0
  29. taskuary/pm.py +310 -0
  30. taskuary/policy.py +59 -0
  31. taskuary/proof.py +194 -0
  32. taskuary/proposals.py +132 -0
  33. taskuary/reports.py +508 -0
  34. taskuary/reshape.py +201 -0
  35. taskuary/responder.py +199 -0
  36. taskuary/routing.py +115 -0
  37. taskuary/scopes.py +88 -0
  38. taskuary/server.py +1506 -0
  39. taskuary/store.py +711 -0
  40. taskuary/templates/coder.md +35 -0
  41. taskuary/templates/digest.md +5 -0
  42. taskuary/templates/learned.md +31 -0
  43. taskuary/templates/soul.md +44 -0
  44. taskuary/templates/style.md +14 -0
  45. taskuary/templates/triage.md +22 -0
  46. taskuary/terminal.py +971 -0
  47. taskuary/toil.py +57 -0
  48. taskuary/triage.py +123 -0
  49. taskuary/verdicts.py +73 -0
  50. taskuary/web/assets/index-6GBZ9nXN.css +32 -0
  51. taskuary/web/assets/index-Cjj87C2X.js +401 -0
  52. taskuary/web/favicon.ico +0 -0
  53. taskuary/web/favicon.png +0 -0
  54. taskuary/web/index.html +25 -0
  55. taskuary/whatsapp/bridge.mjs +101 -0
  56. taskuary/whatsapp/package.json +11 -0
  57. taskuary-0.2.0.dist-info/METADATA +424 -0
  58. taskuary-0.2.0.dist-info/RECORD +62 -0
  59. taskuary-0.2.0.dist-info/WHEEL +5 -0
  60. taskuary-0.2.0.dist-info/entry_points.txt +3 -0
  61. taskuary-0.2.0.dist-info/licenses/LICENSE +21 -0
  62. taskuary-0.2.0.dist-info/top_level.txt +1 -0
taskuary/blackboard.py ADDED
@@ -0,0 +1,150 @@
1
+ """Agents that know about each other. The board IS the blackboard: who is working which
2
+ task in which checkout, and what files each has actually modified so far - read off git and
3
+ the run trace, never off a plan (agents predicting their own scope get it wrong; their
4
+ tracks do not). A new agent in the SAME checkout gets that picture in its seed prompt (a
5
+ peer in another repo is none of its business, and its tokens); a task LIKELY to collide
6
+ queues behind the one already working - first agent in has control.
7
+ """
8
+ import json, os, re, threading, time
9
+ from loguru import logger
10
+
11
+ from .store import task_ref
12
+
13
+
14
+ def norm(p): return os.path.normcase(os.path.normpath(p or ''))
15
+
16
+
17
+ def dirty(cwd) -> set:
18
+ """Uncommitted paths in a checkout, per git itself."""
19
+ from .agents import _git
20
+ return {l[3:].strip().strip('"') for l in _git(cwd, 'status', '--porcelain').splitlines() if len(l) > 3}
21
+
22
+
23
+ # what a headless run touched, read off its own live trace ("→ Edit: path" lines)
24
+ _TOUCH = re.compile(r'^→ (?:Edit|Write|MultiEdit|NotebookEdit)\w*: (.+)$', re.M)
25
+
26
+ def trace_files(trace_json: str) -> list:
27
+ try: evs = json.loads(trace_json or '[]')
28
+ except ValueError: return []
29
+ seen = []
30
+ for e in evs:
31
+ if e.get('kind') != 'live': continue
32
+ for m in _TOUCH.finditer(str(e.get('detail') or '')):
33
+ p = m.group(1).strip()
34
+ if p and p not in seen: seen.append(p)
35
+ return seen[:20]
36
+
37
+
38
+ def target_cwd(store, tid: int, agent: str) -> str:
39
+ """Where WOULD this task's session open? guess_repo + the profile's paths, minus the disk
40
+ search open_session may still run - close enough for routing, never used to refuse."""
41
+ from . import terminal as term
42
+ row = store.get_agent(agent) or {}
43
+ prof = json.loads(row.get('Config') or '{}')
44
+ try: repo, _ = term.guess_repo(store, tid, prof)
45
+ except Exception: repo = None
46
+ return (prof.get('cwd_map') or {}).get(repo or '') or prof.get('cwd') or os.getcwd()
47
+
48
+
49
+ def peers(store, cwd, exclude_tid=None) -> list:
50
+ """Who else is on THIS checkout right now - live pty sessions and headless runs:
51
+ [{tid, ref, title, agent, files, started}]."""
52
+ from . import terminal as term
53
+ out, me = [], norm(cwd)
54
+ for t in term.SESSIONS.values():
55
+ if t.alive and t.task_id and t.task_id != exclude_tid and norm(t.cwd) == me:
56
+ task = store.get_task(t.task_id) or {}
57
+ out.append({'tid': t.task_id, 'ref': task_ref(t.task_id), 'title': task.get('Title') or '',
58
+ 'agent': t.agent or t.label, 'files': t.files(), 'started': t.started})
59
+ for r in store.running_runs():
60
+ if not r.get('TaskId') or r['TaskId'] == exclude_tid or any(p['tid'] == r['TaskId'] for p in out): continue
61
+ prof = json.loads((store.get_agent(r['AgentName']) or {}).get('Config') or '{}')
62
+ if prof.get('cwd') and norm(prof['cwd']) == me:
63
+ task = store.get_task(r['TaskId']) or {}
64
+ out.append({'tid': r['TaskId'], 'ref': task_ref(r['TaskId']), 'title': task.get('Title') or '',
65
+ 'agent': r['AgentName'], 'files': trace_files(r.get('TraceJson')), 'started': r.get('StartedAt')})
66
+ return out
67
+
68
+
69
+ def briefing(store, cwd, exclude_tid=None) -> str:
70
+ """The OTHER AGENTS paragraph of a new agent's prompt. Facts only - task, agent, the files
71
+ it has modified so far - plus the one standing rule: whoever got there first has control."""
72
+ ps = peers(store, cwd, exclude_tid)
73
+ if not ps: return ''
74
+ who = ' | '.join(f"{p['ref']} \"{p['title'][:80]}\" ({p['agent']}) - files it has modified so far: "
75
+ + (', '.join(os.path.basename(x) for x in p['files'][:10]) or 'none yet') for p in ps)
76
+ return ('OTHER AGENTS are working in this same checkout RIGHT NOW: ' + who + '. They were here '
77
+ 'first and have control of their files: never edit, revert, stash or commit them, and '
78
+ 'never use git add -A / git commit -a - stage and commit ONLY files you yourself changed '
79
+ '(git status before committing; uncommitted changes that are not yours are their work in '
80
+ 'progress, leave them exactly as they are).')
81
+
82
+
83
+ OVERLAP_SYSTEM = (
84
+ 'Agents work coding tasks in the same git checkout. A new task is about to start; some are '
85
+ 'already running. Would the new task likely modify any of the SAME FILES as a running one? '
86
+ 'Judge from the descriptions and the files already touched. Output ONLY this JSON: '
87
+ '{"overlap": true/false, "with": "TQ-nnnn or empty", "why": "one short sentence"} - '
88
+ 'when unsure, say false: a wrong true only delays work, and the agents are told about '
89
+ 'each other either way.')
90
+
91
+
92
+ def likely_overlap(store, tid: int, ps: list) -> tuple:
93
+ """(peer, why) when the new task would probably collide with a running one, else (None, '').
94
+ Routing only - a wrong yes waits minutes, a wrong no still gets the briefing - so no AI
95
+ configured (or a bad answer) simply starts the task."""
96
+ from .llm import build_llm
97
+ t = store.get_task(tid) or {}
98
+ try:
99
+ llm = build_llm(store)
100
+ if not llm: return None, ''
101
+ running = '\n'.join(f"- {p['ref']} \"{p['title']}\" - files so far: "
102
+ + (', '.join(p['files'][:15]) or 'none yet') for p in ps)
103
+ out = llm(OVERLAP_SYSTEM, f"New task: \"{t.get('Title') or ''}\" - {str(t.get('Summary') or '')[:1500]}\n\n"
104
+ f'Running in the same checkout:\n{running}', max_tokens=200)
105
+ j = json.loads(re.sub(r'^```(json)?|```$', '', (out or '').strip(), flags=re.M))
106
+ if j.get('overlap'):
107
+ return next((p for p in ps if p['ref'] == str(j.get('with') or '')), ps[0]), str(j.get('why') or '')
108
+ except Exception as e:
109
+ logger.debug(f'overlap check failed for task {tid}: {e}')
110
+ return None, ''
111
+
112
+
113
+ _DRAINING = threading.Lock()
114
+
115
+ def drain(store):
116
+ """A session ended (or a slot freed up): start what was queued, in arrival order. Anything
117
+ whose blocker is still working stays put; anything whose task moved on is just cleared."""
118
+ from . import terminal as term
119
+ from .ingest import AUTO_SESSIONS
120
+ if not _DRAINING.acquire(blocking=False): return
121
+ try:
122
+ for q in store.queued_dispatches():
123
+ if len([t for t in term.SESSIONS.values() if t.alive]) >= AUTO_SESSIONS: return
124
+ b = q.get('BehindTaskId')
125
+ if b and (term.for_task(b) or any(r['TaskId'] == b for r in store.running_runs())): continue
126
+ t = store.get_task(q['TaskId']) or {}
127
+ if t.get('Status') not in ('open', 'in_progress') or term.for_task(q['TaskId']):
128
+ store.clear_dispatch(q['TaskId']); continue
129
+ store.clear_dispatch(q['TaskId'])
130
+ try:
131
+ term.start_on_task(store, q['TaskId'], q.get('Agent') or 'coder', actor='router')
132
+ store.add_comment(q['TaskId'], 'router', 'agent', 'Started from the dispatch queue - '
133
+ + (f'{task_ref(b)} finished with the files it was holding.' if b else 'a session slot freed up.'))
134
+ except Exception as e:
135
+ logger.warning(f'queued dispatch failed for task {q["TaskId"]}: {e}')
136
+ store.add_comment(q['TaskId'], 'router', 'agent', f'Queued start failed: {str(e)[:200]}')
137
+ finally:
138
+ _DRAINING.release()
139
+
140
+
141
+ def drain_later(store, delay: float = 2.0):
142
+ """Drain shortly - called from a dying session's own pump thread, which must not host the
143
+ next session's startup (or block on the drain lock)."""
144
+ tm = threading.Timer(delay, lambda: _safe(store))
145
+ tm.daemon = True
146
+ tm.start()
147
+
148
+ def _safe(store):
149
+ try: drain(store)
150
+ except Exception as e: logger.warning(f'dispatch queue drain failed: {e}')