brindle 0.3.16__tar.gz → 0.3.17__tar.gz

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 (137) hide show
  1. {brindle-0.3.16 → brindle-0.3.17}/PKG-INFO +2 -2
  2. {brindle-0.3.16 → brindle-0.3.17}/README.md +1 -1
  3. {brindle-0.3.16 → brindle-0.3.17}/pyproject.toml +1 -1
  4. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/agents.py +75 -13
  5. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/autopilot.py +324 -85
  6. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cull.py +37 -5
  7. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/db.py +17 -2
  8. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/history.py +96 -4
  9. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/mcp_server.py +17 -8
  10. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/providers.py +31 -0
  11. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/sessions.py +13 -3
  12. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/sidebar_follow.py +3 -0
  13. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/usage.py +21 -2
  14. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/view.py +32 -12
  15. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/watch.py +24 -10
  16. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/watch_costs.py +33 -9
  17. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/workspaces.py +15 -4
  18. {brindle-0.3.16 → brindle-0.3.17}/.gitignore +0 -0
  19. {brindle-0.3.16 → brindle-0.3.17}/LICENSE +0 -0
  20. {brindle-0.3.16 → brindle-0.3.17}/SCHEDULE-A +0 -0
  21. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/__init__.py +0 -0
  22. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/__main__.py +0 -0
  23. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/account.py +0 -0
  24. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/airgap.py +0 -0
  25. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/antigravity.py +0 -0
  26. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/budget.py +0 -0
  27. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/__init__.py +0 -0
  28. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  29. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-codex.md +0 -0
  30. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-fable.md +0 -0
  31. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  32. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  33. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-light.md +0 -0
  34. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer-local.md +0 -0
  35. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/developer.md +0 -0
  36. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/qa.md +0 -0
  37. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  38. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  39. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  40. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/reviewer.md +0 -0
  41. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/subagent.md +0 -0
  42. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_agents/supervisor.md +0 -0
  43. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_rules/__init__.py +0 -0
  44. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_rules/security/backend.md +0 -0
  45. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  46. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/builtin_rules/tests/only.md +0 -0
  47. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/ci_adapters.py +0 -0
  48. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/ci_client.py +0 -0
  49. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/ci_federation.py +0 -0
  50. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/ci_hosts.py +0 -0
  51. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cli.py +0 -0
  52. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cloud_credential.py +0 -0
  53. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cloud_login.py +0 -0
  54. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/codemap.py +0 -0
  55. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/codemap_docs.py +0 -0
  56. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/codex_hook.py +0 -0
  57. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/company_identity.py +0 -0
  58. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/company_login.py +0 -0
  59. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/config.py +0 -0
  60. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/conflicts.py +0 -0
  61. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cost.py +0 -0
  62. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/cost_estimate.py +0 -0
  63. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/demo.py +0 -0
  64. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/detect.py +0 -0
  65. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/doctor.py +0 -0
  66. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/escapes.py +0 -0
  67. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/events.py +0 -0
  68. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/gates.py +0 -0
  69. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/git.py +0 -0
  70. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/guardrails.py +0 -0
  71. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/identity_lock.py +0 -0
  72. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/inbox.py +0 -0
  73. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/keys_cmds.py +0 -0
  74. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/keystore.py +0 -0
  75. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/launch_health.py +0 -0
  76. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/learned_rules.py +0 -0
  77. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/learning.py +0 -0
  78. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/legacy.py +0 -0
  79. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/login_pause.py +0 -0
  80. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/model_access.py +0 -0
  81. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/model_availability.py +0 -0
  82. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/__init__.py +0 -0
  83. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/client.py +0 -0
  84. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/loop.py +0 -0
  85. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/permissions.py +0 -0
  86. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/runner.py +0 -0
  87. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/serve.py +0 -0
  88. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/native/tools.py +0 -0
  89. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pane_auth.py +0 -0
  90. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/permissions.py +0 -0
  91. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pipeline.py +0 -0
  92. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/plugins.py +0 -0
  93. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/policy.py +0 -0
  94. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pool.py +0 -0
  95. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pricing.py +0 -0
  96. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/__init__.py +0 -0
  97. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/_files.py +0 -0
  98. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/account.py +0 -0
  99. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/audit_chain.py +0 -0
  100. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/audit_export.py +0 -0
  101. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/auth.py +0 -0
  102. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/brief_learning.py +0 -0
  103. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/cost_centers.py +0 -0
  104. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/credentials.py +0 -0
  105. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/features.py +0 -0
  106. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/keys.py +0 -0
  107. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/learning.py +0 -0
  108. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/license.py +0 -0
  109. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/loopback.py +0 -0
  110. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/managed_models.py +0 -0
  111. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/org_profiles.py +0 -0
  112. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/orgkey.py +0 -0
  113. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/rollout.py +0 -0
  114. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/route_health.py +0 -0
  115. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/settings_sync.py +0 -0
  116. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/status.py +0 -0
  117. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/team_events.py +0 -0
  118. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/pro/team_policy.py +0 -0
  119. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/procs.py +0 -0
  120. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/profiles.py +0 -0
  121. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/prompt_answer.py +0 -0
  122. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/quota.py +0 -0
  123. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/repo_cmds.py +0 -0
  124. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/repos.py +0 -0
  125. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/rewind.py +0 -0
  126. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/rule_checks.py +0 -0
  127. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/savings.py +0 -0
  128. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/scratch.py +0 -0
  129. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/secrets.py +0 -0
  130. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/self_serve.py +0 -0
  131. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/services.py +0 -0
  132. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/signin_pause.py +0 -0
  133. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/status_cache.py +0 -0
  134. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/tasks.py +0 -0
  135. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/tmux.py +0 -0
  136. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/updates.py +0 -0
  137. {brindle-0.3.16 → brindle-0.3.17}/src/brindle/upgrade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.16
3
+ Version: 0.3.17
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -94,7 +94,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
94
94
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
95
95
 
96
96
  `brindle --version` prints the installed version; the tmux status bar of every brindle
97
- session shows it too (`brindle 0.3.16`). A session started before an upgrade keeps
97
+ session shows it too (`brindle 0.3.17`). A session started before an upgrade keeps
98
98
  running the old code, and shows the old number, until you restart it.
99
99
 
100
100
  ## Quick start
@@ -63,7 +63,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
63
63
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
64
64
 
65
65
  `brindle --version` prints the installed version; the tmux status bar of every brindle
66
- session shows it too (`brindle 0.3.16`). A session started before an upgrade keeps
66
+ session shows it too (`brindle 0.3.17`). A session started before an upgrade keeps
67
67
  running the old code, and shows the old number, until you restart it.
68
68
 
69
69
  ## Quick start
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.16"
3
+ version = "0.3.17"
4
4
  description = "Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -322,10 +322,12 @@ def spawn(
322
322
  autopilot: bool = False,
323
323
  plan_first: bool = False,
324
324
  review_sha: str | None = None,
325
+ claim_goals: bool = True,
325
326
  ) -> Agent:
326
327
  """Start an agent in ``ws``. Workers (handoff/assign) given a ``done_when``
327
328
  finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
328
- agent is a session root that drives toward a goal (see brindle.autopilot)."""
329
+ agent is a session root that drives toward a goal (see brindle.autopilot);
330
+ with ``claim_goals`` it claims a hand-written ``.brindle/goals.md``."""
329
331
  from brindle import autopilot as pilot
330
332
  from brindle import identity_lock
331
333
 
@@ -377,9 +379,9 @@ def spawn(
377
379
  db.update_agent(agent_id, plan_first=1)
378
380
  agent.plan_first = 1
379
381
  if autopilot:
380
- plan = pilot.enable(db, agent_id, ws)
382
+ plan = pilot.enable(db, agent_id, ws, claim=claim_goals)
381
383
  if plan and not prompt:
382
- prompt = agent.task = pilot.kickoff(plan)
384
+ prompt = agent.task = pilot.kickoff(plan, agent_id)
383
385
  db.update_agent(agent_id, task=prompt)
384
386
  if mode == "interactive":
385
387
  from brindle import codemap
@@ -1177,6 +1179,10 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
1177
1179
  if a.status in ("paused", "failed") and (only is None or a.id in only)]
1178
1180
  for a in candidates: # all or nothing: a session never half-resumes under another identity
1179
1181
  identity_lock.ensure(a)
1182
+ if any(a.id == root_id for a in candidates):
1183
+ from brindle import autopilot as pilot
1184
+
1185
+ pilot.reload_goals_file(db, root_id) # edits the person made to this session's goal file
1180
1186
  resumed = []
1181
1187
  for a in candidates:
1182
1188
  if a.status == "failed":
@@ -1506,14 +1512,22 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
1506
1512
  single cheap read, as the dashboard does, leaves it alone. Moving an
1507
1513
  agent to idle also delivers anything queued for it, but only when
1508
1514
  ``samples`` >= 2: a single-sample read must never pop a message into a
1509
- terminal as a side effect of just rendering the dashboard."""
1515
+ terminal as a side effect of just rendering the dashboard.
1516
+
1517
+ An agent whose CLI runs none of brindle's hooks (``hooks_off``: the
1518
+ person's settings turn them off, at launch or later through /hooks, or
1519
+ none reported within HOOKS_GRACE of its input box appearing) has only the
1520
+ screen to go by: it may leave 'starting', a single read may move it from
1521
+ 'idle' to busy, and a ``samples`` >= 2 idle read delivers what's queued
1522
+ even when the status was already idle, since no Stop hook ever will."""
1510
1523
  provider = get_provider(agent.provider)
1511
1524
  if (agent.headless or not provider.uses_hooks
1512
1525
  or agent.status not in ("starting", "idle", "processing", "waiting")):
1513
1526
  return None # a headless pane shows output, not a TUI to read
1514
- if agent.status == "idle" and samples < 2:
1527
+ _sync_hooks_off(db, agent, provider.hooks_disabled(*_settings_scope(db, agent)))
1528
+ if agent.status == "idle" and samples < 2 and not agent.hooks_off:
1515
1529
  return None
1516
- seen = set()
1530
+ seen, ready = set(), True
1517
1531
  for i in range(samples):
1518
1532
  if i:
1519
1533
  time.sleep(gap)
@@ -1525,25 +1539,71 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
1525
1539
  if agent.status == "idle" and state == "busy" and not provider.busy_in_footer(screen):
1526
1540
  state = None
1527
1541
  seen.add(state)
1542
+ ready = ready and provider.input_ready(screen)
1543
+ if (agent.status == "starting" and not agent.hooks_off and ready and provider.announces_start
1544
+ and time.time() - (agent.status_since or agent.created_at) >= HOOKS_GRACE):
1545
+ # Its input box is up, and SessionStart, which runs before it, never reported.
1546
+ # (A CLI without one is marked ready by after_launch instead: see ready.)
1547
+ _sync_hooks_off(db, agent, f"{NO_HOOK_EVENTS} from {provider.name}")
1528
1548
  if len(seen) != 1:
1529
1549
  return None
1530
1550
  state = seen.pop()
1531
1551
  new = {"idle": "idle", "busy": "processing", "waiting": "waiting"}.get(state or "")
1532
- if agent.status == "starting" and new != "waiting":
1552
+ if agent.status == "starting" and new != "waiting" and not agent.hooks_off:
1533
1553
  # Its hooks say when it's ready; the screen only adds that it's
1534
1554
  # stuck on a dialog before they could (e.g. folder trust).
1535
1555
  return None
1536
- if new and new != agent.status:
1556
+ changed = bool(new) and new != agent.status
1557
+ if changed:
1537
1558
  db.set_status(agent.id, new, only_if=agent.status)
1538
1559
  agent.status = new
1539
- if new == "idle" and samples >= 2 and db.pending_count(agent.id):
1540
- try:
1541
- flush(db, agent.id)
1542
- except tmux.TmuxError:
1543
- pass # stays queued; its Stop hook hands it over
1560
+ if (new == "idle" and samples >= 2 and (changed or agent.hooks_off)
1561
+ and db.pending_count(agent.id)):
1562
+ try:
1563
+ flush(db, agent.id)
1564
+ except tmux.TmuxError:
1565
+ pass # stays queued; its Stop hook hands it over
1544
1566
  return new
1545
1567
 
1546
1568
 
1569
+ # How long a hooks CLI may show its input box while still 'starting' before
1570
+ # brindle takes it that none of its hooks run (see screen_status).
1571
+ HOOKS_GRACE = 60.0
1572
+ # How hooks_off reads when no settings explain it, only the missing events.
1573
+ NO_HOOK_EVENTS = "no hook events"
1574
+
1575
+
1576
+ _settings_scopes: dict[tuple[str, float], tuple[str | None, list[str] | None]] = {}
1577
+
1578
+
1579
+ def _settings_scope(db: DB, agent: Agent) -> tuple[str | None, list[str] | None]:
1580
+ """Where ``agent``'s CLI reads its settings: its workspace, and the
1581
+ profile's setting_sources (None: all of them). Kept per agent, as the
1582
+ sidebar asks on every refresh and a running CLI keeps its launch flags."""
1583
+ key = (agent.id, agent.created_at)
1584
+ if key in _settings_scopes:
1585
+ return _settings_scopes[key]
1586
+ from brindle.profiles import ProfileError
1587
+
1588
+ ws = db.get_workspace(agent.workspace_id)
1589
+ try:
1590
+ sources = load_profile(agent.profile, ws.repo_root if ws else None).setting_sources
1591
+ except (KeyError, ProfileError):
1592
+ sources = None
1593
+ scope = _settings_scopes[key] = (ws.path if ws else None), sources
1594
+ return scope
1595
+
1596
+
1597
+ def _sync_hooks_off(db: DB, agent: Agent, why: str | None) -> None:
1598
+ """Record ``why`` its hooks are off; None clears a reason the settings
1599
+ gave (turned back on), but not missing events, which only a hook clears."""
1600
+ if why is None and agent.hooks_off and agent.hooks_off.startswith(NO_HOOK_EVENTS):
1601
+ return
1602
+ if why != agent.hooks_off:
1603
+ db.update_agent(agent.id, hooks_off=why)
1604
+ agent.hooks_off = why
1605
+
1606
+
1547
1607
  def _deliver_to_inbox(db: DB, agent: Agent, message_id: int, text: str,
1548
1608
  sender_id: str | None) -> bool:
1549
1609
  """Send a queued message through Claude Code's own inbox (see
@@ -2661,6 +2721,8 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
2661
2721
  agent = db.get_agent(agent_id)
2662
2722
  if agent is None:
2663
2723
  return None
2724
+ if agent.hooks_off:
2725
+ db.update_agent(agent_id, hooks_off=None) # its hooks run after all: turned back on
2664
2726
  # A hook runs only once the session is going, so its launch is past any
2665
2727
  # startup prompt: a menu on its pane from now on is the agent's own (a
2666
2728
  # question for the user), which no sweep may answer.