Skip to content

Commit 16dba91

Browse files
committed
fix(loop): task text lost when flags precede positional arg
Two bugs made hive run --background loops ignore the task prompt: 1. cmd_loop treated args[0] as the task string unconditionally. When flags came first (--background --max-time 12h "task"), "--background" became the task and the real prompt was dropped. Added _split_task_and_flags() to extract the positional arg regardless of flag ordering. 2. Stop hook loop continuation emitted hookSpecificOutput with additionalContext, but Stop hooks only support systemMessage. Claude Code rejected the JSON, so the agent never received task context on iterations 2+. Matched the format already used by build_nudge_output() and drain_ui_queue().
1 parent a900149 commit 16dba91

4 files changed

Lines changed: 93 additions & 14 deletions

File tree

src/keephive/commands/loop.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ def cmd_loop(args: list[str]) -> None:
4747
if sub == "review":
4848
return _cmd_run_review()
4949

50-
# Otherwise: sub is the task string
51-
_cmd_run_task(sub, args[1:])
50+
# Otherwise: extract task (positional arg) and flags from all args.
51+
# Flags can appear before or after the task string.
52+
task, flag_args = _split_task_and_flags(args)
53+
if not task:
54+
_print_run_help()
55+
return
56+
_cmd_run_task(task, flag_args)
5257

5358

5459
def cmd_loop_extract(args: list[str]) -> None:
@@ -112,6 +117,33 @@ def _find_loop_for_session(session_id: str) -> tuple[dict, Path] | tuple[None, N
112117
# ── Private implementation ───────────────────────────────────────────────────
113118

114119

120+
def _split_task_and_flags(args: list[str]) -> tuple[str | None, list[str]]:
121+
"""Separate the positional task string from flags, regardless of ordering.
122+
123+
Handles: hive run "task" --background --max-time 2h
124+
hive run --background --max-time 2h "task"
125+
hive run --background "task" --max-time 2h
126+
"""
127+
_FLAGS_WITH_VALUE = {"--max-time", "--max", "--at"}
128+
task = None
129+
flag_args: list[str] = []
130+
i = 0
131+
while i < len(args):
132+
a = args[i]
133+
if a in _FLAGS_WITH_VALUE:
134+
flag_args.append(a)
135+
if i + 1 < len(args):
136+
i += 1
137+
flag_args.append(args[i])
138+
elif a.startswith("--"):
139+
flag_args.append(a)
140+
elif task is None:
141+
task = a
142+
# else: extra positional, ignore
143+
i += 1
144+
return task, flag_args
145+
146+
115147
def _sanitize_loop_id(task: str) -> str:
116148
"""Generate a unique loop ID: first meaningful word + YYYYMMDD-HHMMSS."""
117149
from keephive.commands.wander import STOPWORDS

src/keephive/hooks/stop.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ def hook_stop(_args: list[str]) -> None:
110110
"║ Next: hive run review" + " " * (W - 22) + "║\n"
111111
"╚" + "═" * W + "╝"
112112
)
113-
sys.stdout.write(
114-
json.dumps({"hookSpecificOutput": {"additionalContext": completion_msg}})
115-
)
113+
sys.stdout.write(json.dumps({"systemMessage": completion_msg}))
116114
sys.exit(0)
117115

118116
else:
@@ -149,9 +147,7 @@ def hook_stop(_args: list[str]) -> None:
149147
f"PROGRESS CHECK: In one line — what did iteration {iter_n} accomplish?\n"
150148
f"TASK: {req['task']}\n" + time_hint + "─" * 64
151149
)
152-
sys.stdout.write(
153-
json.dumps({"hookSpecificOutput": {"additionalContext": continuation}})
154-
)
150+
sys.stdout.write(json.dumps({"systemMessage": continuation}))
155151
sys.exit(2)
156152

157153
except SystemExit:

tests/test_loop.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,55 @@ def test_parse_flags_no_safe_key(self, hive_env):
159159
assert "safe" not in opts
160160

161161

162+
# ── _split_task_and_flags ─────────────────────────────────────────────────────
163+
164+
165+
class TestSplitTaskAndFlags:
166+
def test_task_first(self, hive_env):
167+
"""Task before flags: hive run 'my task' --background --max-time 2h."""
168+
from keephive.commands.loop import _split_task_and_flags
169+
170+
task, flags = _split_task_and_flags(["my task", "--background", "--max-time", "2h"])
171+
assert task == "my task"
172+
assert "--background" in flags
173+
assert "--max-time" in flags
174+
assert "2h" in flags
175+
176+
def test_flags_first(self, hive_env):
177+
"""Flags before task: hive run --background --max-time 12h 'my task'."""
178+
from keephive.commands.loop import _split_task_and_flags
179+
180+
task, flags = _split_task_and_flags(["--background", "--max-time", "12h", "my task"])
181+
assert task == "my task"
182+
assert "--background" in flags
183+
assert "--max-time" in flags
184+
assert "12h" in flags
185+
186+
def test_flags_mixed(self, hive_env):
187+
"""Task sandwiched: hive run --background 'my task' --max-time 2h."""
188+
from keephive.commands.loop import _split_task_and_flags
189+
190+
task, flags = _split_task_and_flags(["--background", "my task", "--max-time", "2h"])
191+
assert task == "my task"
192+
assert "--background" in flags
193+
194+
def test_no_task(self, hive_env):
195+
"""Only flags, no positional task string."""
196+
from keephive.commands.loop import _split_task_and_flags
197+
198+
task, flags = _split_task_and_flags(["--background", "--max-time", "2h"])
199+
assert task is None
200+
assert len(flags) == 3
201+
202+
def test_task_only(self, hive_env):
203+
"""Just a task, no flags."""
204+
from keephive.commands.loop import _split_task_and_flags
205+
206+
task, flags = _split_task_and_flags(["my task"])
207+
assert task == "my task"
208+
assert flags == []
209+
210+
162211
# ── TestLoopExtractIncludesTodos ──────────────────────────────────────────────
163212

164213

tests/test_nudge.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
from datetime import date
67
from pathlib import Path
78

89

@@ -173,11 +174,12 @@ def test_pending_facts_priority_three(self, hive_env):
173174

174175
# Fresh memory (no stale)
175176
mem = memory_file()
176-
mem.write_text("# Memory\n- Fresh fact [verified:2026-02-20]\n")
177+
today = date.today().isoformat()
178+
mem.write_text(f"# Memory\n- Fresh fact [verified:{today}]\n")
177179

178180
# Pending facts exist
179181
pending = hive_dir() / ".pending-facts.md"
180-
pending.write_text("- Some pending fact [auto:2026-02-20]\n- Another [auto:2026-02-20]\n")
182+
pending.write_text(f"- Some pending fact [auto:{today}]\n- Another [auto:{today}]\n")
181183

182184
from keephive.nudge import _lifecycle_nudge
183185

@@ -191,7 +193,7 @@ def test_unreflected_logs_priority_four(self, hive_env):
191193

192194
# Fresh memory, no pending, no TODOs
193195
mem = memory_file()
194-
mem.write_text("# Memory\n- Fresh fact [verified:2026-02-20]\n")
196+
mem.write_text(f"# Memory\n- Fresh fact [verified:{date.today().isoformat()}]\n")
195197

196198
# Create 8 daily log files
197199
dd = daily_dir()
@@ -215,7 +217,7 @@ def test_fallback_context_prompt(self, tmp_path, monkeypatch):
215217
(hd / "working" / "notes").mkdir()
216218
(hd / "archive").mkdir()
217219
(hd / "working" / "memory.md").write_text(
218-
"# Working Memory\n\n- All facts current [verified:2026-02-17]\n"
220+
f"# Working Memory\n\n- All facts current [verified:{date.today().isoformat()}]\n"
219221
)
220222
monkeypatch.setenv("HIVE_HOME", str(hd))
221223

@@ -235,7 +237,7 @@ def test_fallback_context_tool(self, tmp_path, monkeypatch):
235237
(hd / "working" / "notes").mkdir()
236238
(hd / "archive").mkdir()
237239
(hd / "working" / "memory.md").write_text(
238-
"# Working Memory\n\n- All facts current [verified:2026-02-17]\n"
240+
f"# Working Memory\n\n- All facts current [verified:{date.today().isoformat()}]\n"
239241
)
240242
monkeypatch.setenv("HIVE_HOME", str(hd))
241243

@@ -255,7 +257,7 @@ def test_fallback_context_stop(self, tmp_path, monkeypatch):
255257
(hd / "working" / "notes").mkdir()
256258
(hd / "archive").mkdir()
257259
(hd / "working" / "memory.md").write_text(
258-
"# Working Memory\n\n- All facts current [verified:2026-02-17]\n"
260+
f"# Working Memory\n\n- All facts current [verified:{date.today().isoformat()}]\n"
259261
)
260262
monkeypatch.setenv("HIVE_HOME", str(hd))
261263

0 commit comments

Comments
 (0)