goals.py 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. """The goals bundle (moved verbatim from the pre-split web_stack_test.py;
  2. see test/harness.py for the shared machinery and the runner)."""
  3. from harness import * # noqa: F401,F403 — the shared wire helpers
  4. # =============================================================================
  5. # Bundle: goals — the plan-12 goal lifecycle: creation with branch-name
  6. # uniqueness, the start → planning → plan review → active flow with
  7. # revisions, the goal agent's task fan-out through the task CLI
  8. # callbacks (ownership + creator-only enqueue), the event-driven
  9. # coalesced resume, pause/resume queueing, the completion review with
  10. # branch push state, the cancel cascade, and the human tasks'
  11. # auto-enqueue-after predecessor.
  12. # =============================================================================
  13. def run(ctx):
  14. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  15. app = ctx.app
  16. def create_goal(fields):
  17. act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
  18. rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
  19. if g["title"] == fields["title"]]
  20. return int(rows[0]["id"])
  21. print("== plan 12: goal creation, validation, lifecycle actions ==")
  22. goal1 = create_goal({"project_id": "1", "title": "Ship the flange",
  23. "brief": "Make the flange module production-grade.",
  24. "branch_name": "goal/flange", "priority": "3"})
  25. state = goal_state(goal1)
  26. check(state["status"] == "draft" and state["display"] == "draft"
  27. and state["branch_name"] == "goal/flange"
  28. and state["base_branch"] == "main",
  29. "a goal lands in draft with its branch pair")
  30. check(state.get("start") is not None and state.get("cancel") is not None
  31. and state.get("pause") is None,
  32. "draft offers start + cancel only")
  33. check(state["run_task_id"] == 0 and state["comments"] == [],
  34. "a draft goal has no thread yet (the run task owns it)")
  35. draft_page = http_get_page(auth, "/goals/%d" % goal1)
  36. check('stm-if="goal.run_task_id > 0" stm-action="goal.comment"' in draft_page
  37. and "comments open with its run" in draft_page,
  38. "the draft goal page gates the composer on the run (muted note until then)")
  39. # Branch-name uniqueness: same name, same involved project → refuse.
  40. duplicate_directives = act(auth, slot_public(auth, "/goals", "goals")["create"], {
  41. "project_id": "1", "title": "Clash", "brief": "b", "branch_name": "goal/flange"})
  42. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  43. and "goal/flange" in (d.get("message") or "")
  44. for d in duplicate_directives),
  45. "a live goal's branch name cannot be reused in an involved project (%s)"
  46. % [d.get("message") for d in duplicate_directives if d.get("type") == "notify"])
  47. conn = db()
  48. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  49. " values ('other-repo', 'https://example.com/other.git', 'main',"
  50. " strftime('%s','now'))")
  51. conn.commit()
  52. conn.close()
  53. goal_elsewhere = create_goal({"project_id": "2", "title": "Elsewhere",
  54. "brief": "b", "branch_name": "goal/flange"})
  55. check(goal_elsewhere > 0,
  56. "the same branch name in a disjoint project is fine (one per repo)")
  57. # Brief validation.
  58. brief_directives = act(auth, slot_public(auth, "/goals", "goals")["create"], {
  59. "project_id": "1", "title": "No brief", "brief": " ", "branch_name": "goal/x1"})
  60. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  61. and "brief" in (d.get("message") or "").lower()
  62. for d in brief_directives),
  63. "an empty brief rejects")
  64. # Start: planning + the goal-run internal task.
  65. act(auth, state["start"], {})
  66. state = goal_state(goal1)
  67. check(state["status"] == "planning" and state.get("start") is None,
  68. "start moves the goal to planning")
  69. conn = db()
  70. run_row = conn.execute(
  71. "select id, kind, mode, creator, goal_id, priority, status from tasks"
  72. " where goal_id = ? and kind = 'goal_run'", (goal1,)).fetchone()
  73. conn.close()
  74. check(run_row is not None and run_row[1] == "goal_run"
  75. and run_row[2] == "orchestrate" and run_row[3] == "goal"
  76. and run_row[4] == goal1 and run_row[5] == 3
  77. and run_row[6] in ("queued", "assigned", "working"),
  78. "start creates the goal-run task (orchestrate, priority 3, creator goal,"
  79. " queued)")
  80. run_task = int(run_row[0])
  81. check(goal_state(goal1)["run_task_id"] == run_task,
  82. "the goal state carries the run task id")
  83. check(any(e["kind"] == "started" for e in goal_state(goal1)["events"]),
  84. "the narrative records the start")
  85. print("== feedback: goal-agent runs are hidden from every task listing ==")
  86. # The internal run task never lists anywhere tasks are browsed —
  87. # only the goal page exposes it (worker tasks stay listed). The
  88. # plan script is armed BEFORE the first dispatch so this section's
  89. # waits never consume the round the plan-review flow below needs.
  90. fake.goal_script = [("plan", "# Plan v1\n\n1. Add tests\n2. Ship")]
  91. check(wait_for(lambda: task_state(auth, run_task)["status"]
  92. in ("assigned", "working") or False,
  93. timeout=60, what="run task dispatches") is not None,
  94. "the run task reaches its saddle")
  95. queue_rows = slot_public(auth, "/tasks", "tasks")["tasks"]
  96. check(all(int(t["id"]) != run_task for t in queue_rows),
  97. "the tasks queue never lists the goal-agent run task")
  98. check(all(t["kind"] != "goal_run" for t in queue_rows),
  99. "no goal_run rows leak into the queue at all")
  100. project_state = slot_public(auth, "/projects/1", "project")
  101. check(all(int(t["id"]) != run_task for t in project_state["tasks"]),
  102. "the project page's task list excludes the run task")
  103. dashboard = slot_public(auth, "/", "dashboard")
  104. check(all(int(a["id"]) != run_task for a in dashboard["active_tasks"]),
  105. "the dashboard's active tasks exclude the run task")
  106. check(all(int(i["task_id"]) != run_task for i in dashboard["activity"]),
  107. "the activity timeline never references the run task")
  108. check(wait_for(lambda: int(next(
  109. (s for s in slot_public(auth, "/", "dashboard")["saddles"]
  110. if s["name"] == "fake-saddle"), {})
  111. .get("current_goal_id") or 0) == goal1 or False,
  112. timeout=30, what="saddle card links the goal") is not None,
  113. "a saddle hosting the goal agent reads as supervising the goal")
  114. fake_dash_card = next(s for s in slot_public(auth, "/", "dashboard")["saddles"]
  115. if s["name"] == "fake-saddle")
  116. check(fake_dash_card["current_goal_title"] == "Ship the flange",
  117. "the supervision link carries the goal title")
  118. print("== plan 12: the goal agent plans, plan review, revisions ==")
  119. # (The plan script was armed before the run's first dispatch — see
  120. # the listing-hiding section above — so the first round submits it.)
  121. check(wait_for(lambda: goal_state(goal1)["status"] == "plan_review" or False,
  122. timeout=120, what="goal agent submits its plan") is not None,
  123. "the goal agent's plan push enters plan review")
  124. state = goal_state(goal1)
  125. check(task_state(auth, run_task)["status"] == "blocked",
  126. "the goal-run task parks on the plan review")
  127. # U3's plan-review counterpart: the run-task card never reads as a
  128. # bare "blocked" while a review-shaped block parks it.
  129. check(task_state(auth, run_task)["display"] == "awaiting your review",
  130. "the run-task card reads 'awaiting your review' during the plan review (U3)")
  131. pending = state.get("plan_pending_block")
  132. check(pending and "# Plan v1" in (pending.get("body") or ""),
  133. "the pending plan review carries the submitted markdown")
  134. check(any(b["block_type"] == "plan" and b["is_plan_review"]
  135. for b in state["blocks"]),
  136. "the goal blocks list carries the pending plan block")
  137. print("== feedback: goal visual identity ==")
  138. # A goal never reads as a task at a glance: the nav carries the
  139. # plain Goals entry (5eaab31 dropped the nav-goal accent), the
  140. # list page is iconed, and the dashboard pile's goal cards carry
  141. # their own accent styling.
  142. home_html = http_get_page(auth, "/")
  143. check('<a href="/goals">Goals</a>' in home_html,
  144. "the nav carries the Goals entry beside Tasks")
  145. check(any(p["item_type"] == "goal" for p in
  146. slot_public(auth, "/", "dashboard")["pinned"]),
  147. "the goal's pending block pins onto the dashboard pile")
  148. check("'card goal-card'" in home_html and 'badge-kind-goal' in home_html,
  149. "dashboard goal cards carry the distinct goal styling")
  150. goals_html = http_get_page(auth, "/goals")
  151. check("&#127919;" in goals_html and "goal-icon" in goals_html,
  152. "the goals list page carries the goal icon")
  153. goal_page_html = http_get_page(auth, "/goals/%d" % goal1)
  154. check('class="card goal-head"' in goal_page_html and "goal-page" in goal_page_html,
  155. "the goal page carries the goal-accented identity chrome")
  156. # Reject with feedback: the agent revises.
  157. fake.goal_script = [("plan", "# Plan v2\n\n1. Add tests\n2. Add docs\n3. Ship")]
  158. reject_resume_index = len(fake.goal_assignments)
  159. act(auth, pending["answer_action"], {"answer": "Add documentation please"})
  160. check(wait_for(lambda: goal_state(goal1)["status"] == "plan_review" or False,
  161. timeout=60, what="revised plan enters review") is not None,
  162. "feedback sends the agent back to planning; the revision re-enters review")
  163. # The rejection resume carried NO context_update (nothing in the
  164. # instruction context had changed).
  165. check(all("context_update" not in p
  166. for p in fake.goal_assignments[reject_resume_index:]),
  167. "a resume without context changes carries no context_update")
  168. state = goal_state(goal1)
  169. revisions = state["plan_revisions"]
  170. # Newest-first since the ordering sweep (73dcec9): index 1 is the
  171. # older rejected revision, index 0 the pending re-submission.
  172. check(len(revisions) == 2
  173. and revisions[1]["answered"] and "Add documentation please" in revisions[1]["answer"]
  174. and not revisions[0]["answered"],
  175. "rejected revisions stay as answered history (historical-blocks data)")
  176. # Plan 16 S1 pin: goals.plan is accept-gated — the pending revision
  177. # lives only in its block; nothing lands on the goal before accept.
  178. conn = db()
  179. plan_column = conn.execute(
  180. "select plan from goals where id = ?", (goal1,)).fetchone()[0]
  181. conn.close()
  182. check((plan_column is None or plan_column == "")
  183. and (state["plan"] or "") == "",
  184. "goals.plan stays empty while the revision is only pending (accept-gated, S1)")
  185. # Approve: goal.plan = v2, status active, agent resumes and parks.
  186. # Plan 16 S2: the accept posts the verdict MARKER — the server
  187. # resolves the goal-plan kind and owns the constant.
  188. fake.goal_script = [("suspend",)]
  189. pending = state["plan_pending_block"]
  190. act(auth, pending["answer_action"], {"verdict": "accept"})
  191. check(wait_for(lambda: goal_state(goal1)["status"] == "active" or False,
  192. timeout=60, what="approved plan activates the goal") is not None,
  193. "approving the plan activates the goal")
  194. state = goal_state(goal1)
  195. check(state["plan"] == "# Plan v2\n\n1. Add tests\n2. Add docs\n3. Ship",
  196. "the approved plan markdown is stored on the goal (the accept verdict's write)")
  197. # The approval resume carries the plan delta as a context_update
  198. # turn (the prompt-cache-friendly replacement for the daemon's
  199. # old system-prompt refresh).
  200. check(wait_for(lambda: len(fake.goal_assignments) > 0 and
  201. "context_update" in fake.goal_assignments[-1] or False,
  202. timeout=30, what="approval resume carries context_update") is not None,
  203. "the plan-approval resume carries a context_update turn")
  204. approval_update = fake.goal_assignments[-1]["context_update"]
  205. check("Plan approved" in approval_update and "# Plan v2" in approval_update,
  206. "the context_update summarises the approved plan markdown")
  207. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  208. or False, timeout=60, what="goal agent parks after approval") is not None,
  209. "the resumed goal agent parks (saddle suspend)")
  210. print("== plan 12: task fan-out via the task CLI callbacks ==")
  211. # The goal agent (parked on the plan review — D4: its session row
  212. # closed 'parked', still the record the CLI keys on) creates,
  213. # enqueues and deletes its tasks through /api/saddle/task/*.
  214. conn = db()
  215. agent_session = conn.execute(
  216. "select max(id) from sessions where task_id = ?",
  217. (run_task,)).fetchone()[0]
  218. conn.close()
  219. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  220. "session_id": agent_session, "title": "Flange tests",
  221. "mandate": "Write the test suite for the flange module.",
  222. "mode": "test"})
  223. check(status == 200 and body.get("ok") is True,
  224. "task create under the goal succeeds (%s)" % body.get("error"))
  225. fan_task = int(body["task_id"])
  226. detail = task_state(auth, fan_task)
  227. check(detail["status"] == "draft" and detail["mode"] == "test"
  228. and detail["creator"] == "goal" and detail.get("goal_id") == goal1,
  229. "the created task is a draft goal task with its mode and creator")
  230. check(detail["task_branch"] == "task/%d-%d" % (goal1, fan_task)
  231. and detail["branch_from"] == "goal/flange",
  232. "goal worker tasks get ephemeral branches cut from the goal branch")
  233. check(any(e["kind"] == "task-created" and int(e["task_id"]) == fan_task
  234. for e in goal_state(goal1)["events"]),
  235. "the narrative records the agent's task creation")
  236. # Draft until the agent enqueues; humans may not (creator-only).
  237. check(detail.get("enqueue") is None,
  238. "the human Enqueue action is not offered for goal tasks (creator-only)")
  239. status, body = saddle_api("POST", "/api/saddle/task/enqueue", fake_token, {
  240. "session_id": agent_session, "task_id": fan_task})
  241. check(status == 200 and task_state(auth, fan_task)["status"] == "queued",
  242. "task enqueue by the goal agent queues the draft")
  243. # Ownership + shape errors, verbatim for the CLI.
  244. status, body = saddle_api("POST", "/api/saddle/task/enqueue", fake_token, {
  245. "session_id": agent_session, "task_id": 999999})
  246. check(status == 400 and "No task exists" in (body.get("error") or ""),
  247. "enqueue of a missing task errors verbatim")
  248. create_queued(auth, {"project_id": "1", "title": "Innocent bystander", "priority": "4"})
  249. bystander = task_id_of("Innocent bystander")
  250. status, body = saddle_api("POST", "/api/saddle/task/delete", fake_token, {
  251. "session_id": agent_session, "task_id": bystander})
  252. check(status == 400 and "does not belong to your goal" in (body.get("error") or ""),
  253. "the goal agent cannot touch tasks outside its goal")
  254. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  255. "session_id": agent_session, "title": "Bad mode", "mandate": "m", "mode": "party"})
  256. check(status == 400 and "valid modes" in (body.get("error") or ""),
  257. "an unknown mode rejects with the valid list")
  258. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  259. "session_id": agent_session, "title": "Bad tag", "mandate": "m",
  260. "capabilities": ["warp-drive"]})
  261. check(status == 400 and "warp-drive" in (body.get("error") or "")
  262. and "valid tags" in (body.get("error") or ""),
  263. "an unknown capability tag rejects naming the valid set")
  264. # A plain task's session is not a goal-agent identity.
  265. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token,
  266. {"session_id": 999999, "title": "x", "mandate": "m"})
  267. check(status == 400 and "session" in (body.get("error") or ""),
  268. "an unknown session_id errors verbatim")
  269. # Delete: draft + owned.
  270. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  271. "session_id": agent_session, "title": "Scratch task", "mandate": "m"})
  272. scratch = int(body["task_id"])
  273. status, body = saddle_api("POST", "/api/saddle/task/delete", fake_token, {
  274. "session_id": agent_session, "task_id": scratch})
  275. check(status == 200 and not any(
  276. t["id"] == scratch for t in slot_public(auth, "/tasks", "tasks")["tasks"]),
  277. "task delete removes the owned draft")
  278. status, body = saddle_api("POST", "/api/saddle/task/delete", fake_token, {
  279. "session_id": agent_session, "task_id": fan_task})
  280. check(status == 400 and "draft" in (body.get("error") or ""),
  281. "deleting a queued task refuses (draft only)")
  282. print("== plan 12: goal context in the worker assignment + coalesced resume ==")
  283. # The queued fan-out task dispatches to the daemon with the goal
  284. # reference + ephemeral branch; its completion schedules the parked
  285. # goal agent's resume (>= SADDLE_GOAL_RESUME_S after the event).
  286. check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == fan_task
  287. for p in fake.start_payloads) or False,
  288. timeout=60, what="fan-out task dispatches") is not None,
  289. "the goal worker task dispatches")
  290. fan_payload = [p for p in fake.start_payloads
  291. if int(p.get("task", {}).get("id") or 0) == fan_task][-1]
  292. goal_ctx = fan_payload.get("goal") or {}
  293. check(goal_ctx.get("id") == goal1 and goal_ctx.get("branch") == "goal/flange"
  294. and goal_ctx.get("base_branch") == "main"
  295. and "flange module" in goal_ctx.get("brief", "")
  296. and "# Plan v2" in goal_ctx.get("plan", ""),
  297. "the worker assignment carries the goal context (read-only brief + plan)")
  298. check(fan_payload.get("ephemeral_branch") == "task/%d-%d" % (goal1, fan_task)
  299. and fan_payload.get("kind") == "task" and fan_payload.get("mode") == "test"
  300. and fan_payload.get("creator") == "goal",
  301. "the worker assignment carries the ephemeral branch + identity triplet")
  302. check(wait_task_done(auth, fan_task, timeout=60) is not None,
  303. "the goal worker task completes")
  304. check(any(e["kind"] == "task-done" and int(e["task_id"]) == fan_task
  305. for e in goal_state(goal1)["events"]),
  306. "the narrative records the task's completion")
  307. goal_runs_before = len(fake.goal_assignments)
  308. check(wait_for(lambda: len(fake.goal_assignments) > goal_runs_before or False,
  309. timeout=30, what="parked goal agent resumes") is not None,
  310. "the task-terminal event resumes the parked goal agent")
  311. resume_payload = fake.goal_assignments[-1]
  312. notice = resume_payload.get("resume_notice") or ""
  313. check("Task statuses" in notice and "Flange tests" in notice
  314. and "done" in notice,
  315. "the resume notice batches ALL plan task statuses")
  316. check("New completion reports" in notice and "fake daemon finished" in notice,
  317. "the resume notice carries the new completion report")
  318. check("context_update" not in resume_payload,
  319. "a later resume with no context changes omits context_update entirely")
  320. check(resume_payload.get("kind") == "goal_run"
  321. and resume_payload.get("mode") == "orchestrate",
  322. "the resumed goal agent carries its identity triplet")
  323. # The resumed agent parked again (script): two events at once must
  324. # coalesce into ONE resume within the window.
  325. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  326. or False, timeout=60, what="goal agent re-parks") is not None,
  327. "the resumed goal agent parks again")
  328. print("== plan 16 S3: delta resume notices — one-line statuses, bodies once ==")
  329. # A MULTI-LINE completion lands while the agent is parked. The next
  330. # notice lists every status ONE-LINE (first line of the summary
  331. # only) and embeds each NEW completion's full body exactly once —
  332. # no statuses-section/reports-section double embedding. (Timestamps
  333. # are second-granular: each fixture waits out the park's second so
  334. # completed_at is strictly newer than suspended_at.)
  335. def run_park_second_passed():
  336. conn = db()
  337. try:
  338. row = conn.execute("select suspended_at from tasks where id = ?",
  339. (run_task,)).fetchone()
  340. if not row or not row[0]:
  341. return False
  342. return time.time() > float(row[0]) + 1.05
  343. finally:
  344. conn.close()
  345. check(wait_for(run_park_second_passed, timeout=30,
  346. what="S3 park second passes") is not None,
  347. "the park stamp is in a strictly older second")
  348. conn = db()
  349. conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
  350. " priority, summary, completed_at, created_at) values (1, ?, 'task',"
  351. " 'Multiline report', 'done', 5,"
  352. " 'Headline: the flange calibrates\n\nBody: the full report with all"
  353. " the detail lines.',"
  354. " strftime('%s','now'),"
  355. " strftime('%s','now','-30 seconds'))", (goal1,))
  356. conn.commit()
  357. conn.close()
  358. s3_before = len(fake.goal_assignments)
  359. check(wait_for(lambda: len(fake.goal_assignments) > s3_before or False,
  360. timeout=60, what="S3 delta resume fires") is not None,
  361. "the multiline completion resumes the parked agent")
  362. s3_notice = fake.goal_assignments[-1].get("resume_notice") or ""
  363. s3_statuses = s3_notice.split("## Task statuses", 1)[1] \
  364. .split("## New completion reports", 1)[0]
  365. check("Headline: the flange calibrates" in s3_statuses
  366. and "the full report with all the detail lines" not in s3_statuses,
  367. "the statuses section carries ONE LINE per task (the summary's first line)")
  368. check(s3_notice.count("the full report with all the detail lines.") == 1
  369. and "### #" in s3_notice,
  370. "each new completion's full body embeds EXACTLY ONCE (no double embedding)")
  371. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  372. or False, timeout=60, what="S3 agent re-parks") is not None,
  373. "the S3 agent re-parks")
  374. # Size constant per event, not cumulative: the NEXT resume (armed
  375. # with a fresh single-line completion) reports only the NEW body —
  376. # the consumed multiline report never re-embeds.
  377. check(wait_for(run_park_second_passed, timeout=30,
  378. what="S3 second park second passes") is not None,
  379. "the second park stamp is in a strictly older second")
  380. conn = db()
  381. conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
  382. " priority, summary, completed_at, created_at) values (1, ?, 'task',"
  383. " 'Fresh completion', 'done', 5, 'fresh one-liner',"
  384. " strftime('%s','now'),"
  385. " strftime('%s','now','-30 seconds'))", (goal1,))
  386. conn.commit()
  387. conn.close()
  388. s3_more = len(fake.goal_assignments)
  389. check(wait_for(lambda: len(fake.goal_assignments) > s3_more or False,
  390. timeout=60, what="S3 second delta resume") is not None,
  391. "the fresh completion resumes the agent again")
  392. s3_next = fake.goal_assignments[-1].get("resume_notice") or ""
  393. check("the full report with all the detail lines." not in s3_next
  394. and "### #" in s3_next and "fresh one-liner" in s3_next,
  395. "the consumed completion's body never re-embeds (notice size constant per event)")
  396. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  397. or False, timeout=60, what="S3 agent parks a third time") is not None,
  398. "the S3 agent parks for the pause section")
  399. print("== feedback: goal page parity — progress feed, current activity, runs ==")
  400. # The goal agent's runs feed the page's live surfaces: progress
  401. # lines (green latest-first feed), the parked round's newest
  402. # activity line and its hosting saddle. Parked rounds keep their
  403. # session record (closed 'parked', D4) — the page reads the
  404. # newest row, so fixtures land as rows without disturbing the
  405. # scripted daemon.
  406. conn = db()
  407. open_run_session = conn.execute(
  408. "select max(id) from sessions where task_id = ?",
  409. (run_task,)).fetchone()[0]
  410. conn.execute(
  411. "insert into events (session_id, seq, kind, actor, scope_type, scope_id,"
  412. " payload, created_at) values (?, 8001, 'progress', 'model', 'task', ?,"
  413. " ?, strftime('%s','now'))",
  414. (open_run_session, run_task, json.dumps({"text": "Overseeing the flange fan-out"})))
  415. conn.execute(
  416. "insert into events (session_id, seq, kind, actor, scope_type, scope_id,"
  417. " payload, created_at) values (?, 8002, 'tool_call', 'model', 'task', ?,"
  418. " ?, strftime('%s','now'))",
  419. (open_run_session, run_task,
  420. json.dumps({"tool": "run_command",
  421. "args": {"command": "git -C demo log --oneline -5"}})))
  422. conn.commit()
  423. conn.close()
  424. # A manual refresh records the daemon's health snapshot (the
  425. # parked daemon answers /status with its suspended state).
  426. act(auth, saddle_card(auth, "fake-saddle")["refresh"], {})
  427. state = goal_state(goal1)
  428. check(state["progress"] and state["progress"][0]["summary"]
  429. == "Overseeing the flange fan-out",
  430. "the goal page carries the agent's live progress feed")
  431. check(state["current_command"] == "run_command: git -C demo log --oneline -5"
  432. and state["current_command_running"] is True,
  433. "the current activity shows the open round's running command")
  434. check(state["agent_saddle"] == "fake-saddle"
  435. and "fake-saddle" in state["saddle_health"],
  436. "the assigned agent saddle and its health display")
  437. check(state["runs"] and state["runs"][0]["saddle_name"] == "fake-saddle"
  438. and state["runs"][0]["terminal_url"] == "/tasks/%d/log" % run_task,
  439. "the run table carries host saddles and the event log link")
  440. goal_page = http_get_page(auth, "/goals/%d" % goal1)
  441. check('stm-markdown="goal.brief"' in goal_page
  442. and 'stm-markdown="goal.plan"' in goal_page
  443. and 'stm-markdown="comment.body"' in goal_page
  444. and 'stm-markdown="event.text"' in goal_page
  445. and 'stm-markdown="block.body"' in goal_page,
  446. "every long-form goal body renders through stm-markdown")
  447. check('stm-attribute.href="run.terminal_url" target="_blank" rel="noopener"' in goal_page,
  448. "the run table's terminal links open in a new tab")
  449. check("progress-latest" in goal_page and "Agent progress" in goal_page,
  450. "the green progress feed renders on the goal page")
  451. # Plan revision history keeps its verdicts (approved + rejected).
  452. revisions = goal_state(goal1)["plan_revisions"]
  453. # Newest-first since the ordering sweep (73dcec9): index 0 is the
  454. # approved revision, index 1 the older rejected one.
  455. check(revisions[1]["answered"] is True and revisions[1]["approved"] is False
  456. and revisions[0]["approved"] is True,
  457. "plan revision history badges approved and rejected verdicts")
  458. print("== plan 12: pause freezes supervision; events coalesce on resume ==")
  459. state = goal_state(goal1)
  460. act(auth, state["pause"], {})
  461. check(goal_state(goal1)["status"] == "paused"
  462. and goal_state(goal1).get("resume") is not None,
  463. "an active goal pauses and offers resume")
  464. # Terminal events under a paused goal queue without waking anybody.
  465. # (The real completion path — complete_task — records the goal's
  466. # task-done narrative event; the resume machinery keys on exactly
  467. # those terminal kinds, never on the agent's own enqueue narrative.)
  468. conn = db()
  469. conn.execute("insert into tasks (project_id, goal_id, kind, title, status, priority,"
  470. " summary, completed_at, created_at) values (1, ?, 'task',"
  471. " 'Quiet task', 'done', 5, 'finished while paused',"
  472. " strftime('%s','now'), strftime('%s','now'))", (goal1,))
  473. quiet_id = conn.execute("select max(id) from tasks").fetchone()[0]
  474. conn.commit()
  475. conn.close()
  476. paused_runs = len(fake.goal_assignments)
  477. check(quiet_window(quiet_seconds(),
  478. lambda: len(fake.goal_assignments) == paused_runs,
  479. "paused goal does not resume"),
  480. "a paused goal's events queue without scheduling a resume")
  481. state = goal_state(goal1)
  482. act(auth, state["resume"], {})
  483. check(goal_state(goal1)["status"] == "active",
  484. "resume returns the goal to active")
  485. check(wait_for(lambda: len(fake.goal_assignments) > paused_runs or False,
  486. timeout=30, what="queued events flush on resume") is not None,
  487. "the queued events coalesce into the post-resume wake")
  488. check("finished while paused" in (fake.goal_assignments[-1].get("resume_notice") or ""),
  489. "the flushed notice reports the task that finished while paused")
  490. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  491. or False, timeout=60, what="goal agent parks again") is not None,
  492. "the goal agent parks again after the flush")
  493. # The watermark consumed the flushed transition: with nothing new
  494. # under the goal, no further resume fires (parked stays parked).
  495. flushed_assignments = len(fake.goal_assignments)
  496. check(quiet_window(quiet_seconds(),
  497. lambda: len(fake.goal_assignments) == flushed_assignments,
  498. "consumed watermark never re-fires"),
  499. "the consumed watermark never re-fires a resume")
  500. print("== plan 12: completion review, push state, accept ==")
  501. # The daemon exposes goal-branch push state on /status; the web
  502. # caches it on the goal when it probes the goal agent's saddle.
  503. def status_extra():
  504. return None
  505. fake.status_extra = {"goal_branches": [
  506. {"branch": "goal/flange", "pushed": True},
  507. {"branch": "goal/other-repo", "pushed": False}]}
  508. fake_card = saddle_card(auth, "fake-saddle")
  509. act(auth, fake_card["refresh"], {})
  510. check(wait_for(lambda: goal_state(goal1).get("push_branches") or False,
  511. timeout=30, what="push state lands on the goal") is not None,
  512. "the daemon's /status goal_branches exposure caches on the goal")
  513. branches = goal_state(goal1)["push_branches"]
  514. check({(b["branch"], b["pushed"]) for b in branches}
  515. == {("goal/flange", True), ("goal/other-repo", False)},
  516. "the push state renders per branch")
  517. # the daemon's real shape carries repo too — multi-repo rows stay
  518. # distinguishable as "repo: branch" labels
  519. fake.status_extra = {"goal_branches": [
  520. {"repo": "repoa", "branch": "goal/flange", "pushed": True},
  521. {"repo": "repob", "branch": "goal/flange", "pushed": False}]}
  522. act(auth, fake_card["refresh"], {})
  523. check(wait_for(lambda: {b["branch"] for b in goal_state(goal1).get("push_branches") or []}
  524. >= {"repoa: goal/flange"}, timeout=30,
  525. what="repo-qualified push state lands") is not None,
  526. "repo-qualified goal_branches rows render as 'repo: branch'")
  527. fake.status_extra = None
  528. # `saddle done` in goal context: the completion review.
  529. fake.goal_script = [("done", "# Completion\n\nThe flange ships.")]
  530. # Wake the parked agent: a terminal transition newer than the
  531. # watermark (the pause-flush consumed everything up to it, so arm
  532. # one directly with a fresh row).
  533. conn = db()
  534. conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
  535. " priority, completed_at, created_at) values (1, ?, 'task',"
  536. " 'Warmup', 'done', 5,"
  537. " strftime('%s','now'),"
  538. " strftime('%s','now','-30 seconds'))", (goal1,))
  539. conn.commit()
  540. conn.close()
  541. check(wait_for(lambda: goal_state(goal1)["status"] == "completion_review" or False,
  542. timeout=60, what="goal agent submits its completion") is not None,
  543. "`saddle done` in goal context enters completion review")
  544. state = goal_state(goal1)
  545. # Plan 16 S1 (canonical-on-accept): while the review is PENDING the
  546. # goal row carries NO report — the document lives only in its
  547. # block; completion_report is written solely by the accept verdict.
  548. conn = db()
  549. pending_report_column = conn.execute(
  550. "select completion_report from goals where id = ?", (goal1,)).fetchone()[0]
  551. conn.close()
  552. check((pending_report_column is None or pending_report_column == "")
  553. and (state["completion_report"] or "") == ""
  554. and "# Completion" in (state["completion_pending_block"].get("body") or "")
  555. and state["completion_pending"] is True
  556. and state["completion_pending_block"] is not None
  557. and state["completion_pending_block"] is not False,
  558. "the pending report lives ONLY in its block (goal row clean, S1)")
  559. check(task_state(auth, run_task)["status"] == "blocked",
  560. "the goal-run task parks on the completion review")
  561. # Plan 16 S5 (U3): label reconciliation — the goal awaiting its
  562. # completion review reads "awaiting your review" and the run-task
  563. # status card agrees (never a bare "blocked" for a review park).
  564. check(goal_state(goal1)["display"] == "awaiting your review"
  565. and task_state(auth, run_task)["display"] == "awaiting your review",
  566. "the goal and the run-task card agree: 'awaiting your review' (U3)")
  567. check(state["compare_url"].endswith("/compare/main...goal/flange"),
  568. "the completion review links the compare view")
  569. # Reject first: feedback returns the goal to active. Plan 16 S2:
  570. # the goal page's reject posts raw `feedback` — the SERVER wraps
  571. # it with the completion-review prefix (byte-identical to the
  572. # historical client-built answer).
  573. fake.goal_script = [("suspend",)]
  574. act(auth, state["completion_pending_block"]["answer_action"],
  575. {"feedback": "Not done — the docs are missing"})
  576. check(wait_for(lambda: goal_state(goal1)["status"] == "active" or False,
  577. timeout=30, what="rejected completion reactivates") is not None,
  578. "rejecting the completion returns the goal to active")
  579. conn = db()
  580. rejected_answer = conn.execute(
  581. "select answer from task_blocks where task_id = ?"
  582. " and type = 'completion' order by id desc", (run_task,)).fetchone()[0]
  583. conn.close()
  584. check(rejected_answer
  585. == "Review feedback received:\n\nNot done — the docs are missing",
  586. "the completion rejection wraps feedback with the stable prefix (%r)"
  587. % rejected_answer)
  588. conn = db()
  589. rejected_report = conn.execute(
  590. "select completion_report from goals where id = ?", (goal1,)).fetchone()[0]
  591. conn.close()
  592. check(rejected_report is None or rejected_report == "",
  593. "reject/revise leaves the goal row clean (no report leaked, S1)")
  594. # Round 2: submit again and accept.
  595. fake.goal_script = [("done", "# Completion v2\n\nThe flange ships, with docs.")]
  596. conn = db()
  597. conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
  598. " priority, completed_at, created_at) values (1, ?, 'task',"
  599. " 'Warmup2', 'done', 5,"
  600. " strftime('%s','now'),"
  601. " strftime('%s','now','-30 seconds'))", (goal1,))
  602. conn.commit()
  603. conn.close()
  604. check(wait_for(lambda: goal_state(goal1)["status"] == "completion_review" or False,
  605. timeout=60, what="resubmitted completion") is not None,
  606. "the agent resubmits its completion after feedback")
  607. state = goal_state(goal1)
  608. # Regression (plan-15 followup): the daemon's done-park ALSO pushes a
  609. # review-kind block payload (question "Goal completion review", the
  610. # real daemon's wording) after the done entry raised the completion
  611. # row — the two paths must converge on ONE row, not twin reviews.
  612. fake.push_results("blocked", block={
  613. "question": "Goal completion review", "type": "review",
  614. "body": "# Completion v2\n\nThe flange ships, with docs.",
  615. "options": None})
  616. conn = db()
  617. rows = conn.execute(
  618. "select count(*) from task_blocks where task_id = ?"
  619. " and answer is null and type in ('completion','review')",
  620. (run_task,)).fetchone()[0]
  621. conn.close()
  622. check(rows == 1,
  623. "the daemon's review-kind completion push adopts the pending row (no twin)")
  624. state = goal_state(goal1)
  625. review_family_in_open = [b for b in state.get("open_blocks", [])
  626. if b.get("is_completion_review")]
  627. check(state["completion_pending"] is True and len(review_family_in_open) == 1,
  628. "the pending completion review renders ONCE — as the rich attention block")
  629. fake.pending_block = None
  630. # Raise-side single-row invariant (the render guard and the
  631. # twin-answer sweep are gone — fresh DBs cannot hold pre-dedupe
  632. # twins, and 4884259's raise-side cross-dedupe keeps it that way):
  633. # a REDELIVERED review-kind completion push adopts the same pending
  634. # row instead of stacking a twin.
  635. fake.push_results("blocked", block={
  636. "question": "Goal completion review", "type": "review",
  637. "body": "# Completion v2\n\nThe flange ships, with docs.",
  638. "options": None})
  639. fake.pending_block = None
  640. conn = db()
  641. rows = conn.execute(
  642. "select count(*) from task_blocks where task_id = ?"
  643. " and answer is null and type in ('completion','review')",
  644. (run_task,)).fetchone()[0]
  645. conn.close()
  646. check(rows == 1,
  647. "a redelivered review-kind push adopts the pending row (raise-side single-row invariant)")
  648. # Plan 15 C4: the completion-accept hard gate — the live probe
  649. # flags still report repob's branch unpushed, so the operator's
  650. # accept REFUSES with origin behind.
  651. refusal = action_notify(act(auth, state["completion_pending_block"]["answer_action"],
  652. {"answer": "Review approved. Continue with the task."}))
  653. check(goal_state(goal1)["status"] == "completion_review",
  654. "the accept REFUSES while origin is behind (hard gate, live flags)")
  655. # One parser, one label: the gate's refusal names the SAME repo-
  656. # qualified unpushed row the chips render — both surfaces read
  657. # GoalService.parse_goal_branches (the gate-vs-chips divergence is
  658. # structurally gone).
  659. branches = {(b["branch"], b["pushed"]) for b in goal_state(goal1)["push_branches"]}
  660. check("repob: goal/flange" in (refusal or "")
  661. and ("repob: goal/flange", False) in branches
  662. and ("repoa: goal/flange", True) in branches,
  663. "the refusal and the chips read ONE parser (same labels, same flags)")
  664. # Push the branch daemon-side and probe again — the flags flip
  665. # live (no goal-row write), and the accept goes through.
  666. fake.status_extra = {"goal_branches": [
  667. {"repo": "repoa", "branch": "goal/flange", "pushed": True},
  668. {"repo": "repob", "branch": "goal/flange", "pushed": True}]}
  669. act(auth, fake_card["refresh"], {})
  670. check(wait_for(lambda: (goal_state(goal1).get("push_branches") or [])
  671. and all(b["pushed"] for b in goal_state(goal1)["push_branches"])
  672. or False, timeout=30, what="pushed flags land") is not None,
  673. "the push chips derive from the LIVE probe flags")
  674. conn = db()
  675. no_push_column = conn.execute(
  676. "select count(*) from pragma_table_info('goals')"
  677. " where name = 'branch_push_state'").fetchone()[0] == 0
  678. conn.close()
  679. check(no_push_column,
  680. "no stored push snapshot exists (F9: the goal row never records it)")
  681. state = goal_state(goal1)
  682. # The passing accept posts the verdict MARKER (the goal page's
  683. # accept form shape, plan 16 S2) — the server resolves the
  684. # completion kind and stores the stable approval wording.
  685. act(auth, state["completion_pending_block"]["answer_action"],
  686. {"verdict": "accept"})
  687. check(wait_for(lambda: goal_state(goal1)["status"] == "complete" or False,
  688. timeout=30, what="accepted completion completes") is not None,
  689. "accepting the completion completes the goal once origin is current")
  690. state = goal_state(goal1)
  691. conn = db()
  692. accepted_report = conn.execute(
  693. "select completion_report from goals where id = ?", (goal1,)).fetchone()[0]
  694. conn.close()
  695. check(state["terminal"] is True and state.get("cancel") is None
  696. and accepted_report == "# Completion v2\n\nThe flange ships, with docs."
  697. and "# Completion v2" in state["completion_report"],
  698. "a complete goal is terminal with the WINNING block's body as its report (S1)")
  699. check(wait_for(lambda: task_state(auth, run_task)["status"] == "done" or False,
  700. timeout=30, what="goal run task closes") is not None,
  701. "approving the completion closes the goal-run task done")
  702. conn = db()
  703. twins_left = conn.execute(
  704. "select count(*) from task_blocks where task_id = ?"
  705. " and answer is null and type in ('completion','review')",
  706. (run_task,)).fetchone()[0]
  707. conn.close()
  708. check(twins_left == 0,
  709. "the verdict answered the pending completion-family row (single-row invariant)")
  710. # Plan 16 D4 (web half): the verdict path closes what it closes —
  711. # after the accept NO session row of the run task dangles open,
  712. # and every parked round carries outcome 'parked' (the daemon's
  713. # file-log park brackets name these same rows).
  714. conn = db()
  715. open_after_accept = conn.execute(
  716. "select count(*) from sessions where task_id = ? and ended_at is null",
  717. (run_task,)).fetchone()[0]
  718. parked_rounds = conn.execute(
  719. "select count(*) from sessions where task_id = ? and outcome = 'parked'",
  720. (run_task,)).fetchone()[0]
  721. conn.close()
  722. check(open_after_accept == 0,
  723. "the accept leaves no dangling open session (D4: every park closed its row)")
  724. check(parked_rounds >= 1,
  725. "the run's parked rounds all closed 'parked' (D4 outcome rename)")
  726. print("== plan 15 C3: the narrative derives from the recorded shape ==")
  727. # Content equivalence with the old projection on this run's exact
  728. # shape: every kind the goal_events projection produced, keyed the
  729. # same way, plus the watermark and no-ingest guarantees.
  730. narrative = goal_state(goal1)["events"]
  731. kinds = [e["kind"] for e in narrative]
  732. for expected in ("created", "started", "plan-submitted", "plan-rejected",
  733. "plan-approved", "task-created", "task-done",
  734. "completion-submitted", "completion-rejected", "goal-approved"):
  735. check(expected in kinds,
  736. "the derived narrative carries the %s row" % expected)
  737. check(all(k != "task-enqueued" and k != "resumed" and k != "retry" for k in kinds),
  738. "timestamp-less bookkeeping kinds never return")
  739. stamps = [e["at_display"] for e in narrative if e.get("at_display")]
  740. check(stamps == sorted(stamps, reverse=True),
  741. "the narrative reads newest first")
  742. conn = db()
  743. no_event_table = conn.execute(
  744. "select count(*) from sqlite_master where type = 'table'"
  745. " and name = 'goal_events'").fetchone()[0] == 0
  746. conn.close()
  747. check(no_event_table,
  748. "the goal_events table is gone (no narrative rows on ingest)")
  749. print("== plan 12: cancel cascades to the goal's tasks ==")
  750. goal2 = create_goal({"project_id": "1", "title": "Doomed goal",
  751. "brief": "This goal will be cancelled.",
  752. "branch_name": "goal/doomed"})
  753. fake.goal_script = [("suspend",)]
  754. act(auth, goal_state(goal2)["start"], {})
  755. conn = db()
  756. run2 = conn.execute("select id from tasks where goal_id = ? and kind = 'goal_run'",
  757. (goal2,)).fetchone()[0]
  758. conn.close()
  759. check(wait_for(lambda: task_state(auth, run2)["status"] == "suspended" or False,
  760. timeout=60, what="doomed goal agent parks") is not None,
  761. "the doomed goal's agent parks")
  762. conn = db()
  763. agent_session2 = conn.execute(
  764. "select max(id) from sessions where task_id = ?",
  765. (run2,)).fetchone()[0]
  766. conn.close()
  767. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  768. "session_id": agent_session2, "title": "Doomed draft", "mandate": "m"})
  769. doomed_draft = int(body["task_id"])
  770. doomed_queued_task = None
  771. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  772. "session_id": agent_session2, "title": "Doomed runner", "mandate": "m"})
  773. doomed_runner = int(body["task_id"])
  774. saddle_api("POST", "/api/saddle/task/enqueue", fake_token,
  775. {"session_id": agent_session2, "task_id": doomed_runner})
  776. act(auth, goal_state(goal2)["cancel"], {})
  777. check(goal_state(goal2)["status"] == "cancelled",
  778. "the goal cancels from active (parked-agent) state")
  779. check(task_state(auth, run2)["status"] == "failed",
  780. "the cascade cancels the goal agent's run task")
  781. check(wait_for(lambda: task_state(auth, doomed_runner)["status"] == "failed"
  782. or False, timeout=30, what="cascade cancels the queued task") is not None,
  783. "the cascade cancels the goal's queued task")
  784. check(not any(t["id"] == doomed_draft
  785. for t in slot_public(auth, "/tasks", "tasks")["tasks"]),
  786. "the cascade deletes the goal's draft tasks")
  787. # A cancelled goal frees its branch name.
  788. reuse = create_goal({"project_id": "1", "title": "Reused name",
  789. "brief": "b", "branch_name": "goal/doomed"})
  790. check(reuse > goal2, "a cancelled goal's branch name is reusable")
  791. print("== plan 13 B3: goal failure cascades to non-terminal tasks ==")
  792. # A goal holding a draft, a queued task and a working task: the
  793. # agent's failure cascades like a cancellation — drafts deleted (no
  794. # orphan stays dispatchable), queued/working cancelled — and the
  795. # narrative records the cascade.
  796. collapse = create_goal({"project_id": "1", "title": "Collapse goal",
  797. "brief": "Fails with tasks in flight.",
  798. "branch_name": "goal/collapse"})
  799. fake.goal_script = [("suspend",)]
  800. act(auth, goal_state(collapse)["start"], {})
  801. conn = db()
  802. run3 = conn.execute("select id from tasks where goal_id = ? and kind = 'goal_run'",
  803. (collapse,)).fetchone()[0]
  804. conn.close()
  805. check(wait_for(lambda: task_state(auth, run3)["status"] == "suspended" or False,
  806. timeout=60, what="collapse goal agent parks") is not None,
  807. "the collapse goal's agent parks first")
  808. conn = db()
  809. run3_session = conn.execute(
  810. "select max(id) from sessions where task_id = ?",
  811. (run3,)).fetchone()[0]
  812. conn.close()
  813. def agent_task(title):
  814. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  815. "session_id": run3_session, "title": title, "mandate": "m"})
  816. task_id = int(body["task_id"])
  817. saddle_api("POST", "/api/saddle/task/enqueue", fake_token,
  818. {"session_id": run3_session, "task_id": task_id})
  819. return task_id
  820. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  821. "session_id": run3_session, "title": "Collapse draft", "mandate": "m"})
  822. collapse_draft = int(body["task_id"]) # stays draft (never enqueued)
  823. collapse_queued = agent_task("Collapse queued")
  824. collapse_worker = agent_task("Collapse worker")
  825. # Both queued tasks race for the one saddle; freeze the daemon the
  826. # moment the first dispatches so one stays genuinely WORKING (and
  827. # the other queued) when the goal fails.
  828. fake.hold_state = True
  829. def collapse_working():
  830. for task_id in (collapse_queued, collapse_worker):
  831. if task_state(auth, task_id)["status"] == "working":
  832. return task_id
  833. return False
  834. running_one = wait_for(collapse_working, timeout=60, what="a collapse task works")
  835. check(running_one is not None,
  836. "a collapse task reaches working before the failure")
  837. queued_one = collapse_worker if running_one == collapse_queued else collapse_queued
  838. status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
  839. {"session_id": run3_session, "state": "failed",
  840. "summary": "the foundation crumbled"})
  841. check(status == 200 and goal_state(collapse)["status"] == "failed",
  842. "the agent's failure fails the goal")
  843. check(task_state(auth, run3)["status"] == "failed",
  844. "the failure fails the goal agent's run task")
  845. check(wait_for(lambda: task_state(auth, queued_one)["status"] == "failed"
  846. and task_state(auth, running_one)["status"] == "failed" or False,
  847. timeout=30, what="failure cascade settles") is not None,
  848. "the failure cascade cancels the queued and working tasks")
  849. check(not any(t["id"] == collapse_draft
  850. for t in slot_public(auth, "/tasks", "tasks")["tasks"]),
  851. "the failure cascade deletes the goal's draft tasks")
  852. state = goal_state(collapse)
  853. check(any(e["kind"] == "cascade" for e in state["events"]),
  854. "the narrative records the failure cascade")
  855. check(state["draft_tasks"] == 0 and state["queued_tasks"] == 0,
  856. "nothing under the failed goal stays dispatchable (no orphan drafts)")
  857. check(len([e for e in state["events"] if e["kind"] == "goal-failed"]) == 1,
  858. "the failing goal records exactly one goal-failed event")
  859. # Plan 16 S1: the FAILURE report keeps its goal-side home (failure
  860. # is terminal — no pending duplication exists, so unlike the
  861. # completion report it is written at failure time).
  862. conn = db()
  863. failure_report = conn.execute(
  864. "select completion_report from goals where id = ?", (collapse,)).fetchone()[0]
  865. conn.close()
  866. check(failure_report == "the foundation crumbled",
  867. "fail_goal's failure report keeps its goal-side home (S1)")
  868. # Plan 16 S4: the narrative renders the task-failed rows from the
  869. # terminal summary's lead — the run thread carries NO duplicate
  870. # agent comment of the goal failure text.
  871. failed_rows = [e for e in state["events"] if e["kind"] == "task-failed"]
  872. check(failed_rows
  873. and all(": Cancelled by user" in e["text"] for e in failed_rows),
  874. "task-failed narrative rows render the summary lead (S4: summary is the terminal cache)")
  875. run_comments = [c for c in task_state(auth, run3)["comments"]
  876. if c["author"] == "agent"]
  877. check(not any("the foundation crumbled" in c["body"] for c in run_comments),
  878. "the goal failure text never duplicates as an agent comment (S4)")
  879. fake.hold_state = False
  880. print("== plan 12: auto-enqueue-after for human tasks ==")
  881. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  882. "project_id": "1", "title": "Predecessor task", "priority": "4"})
  883. predecessor = task_id_of("Predecessor task")
  884. enqueue_task(auth, predecessor)
  885. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  886. "project_id": "1", "title": "Successor task", "priority": "4",
  887. "enqueue_after_task_id": str(predecessor)})
  888. successor = task_id_of("Successor task")
  889. check(task_state(auth, successor)["status"] == "draft"
  890. and task_state(auth, successor).get("enqueue_after_task_id") == predecessor,
  891. "a human task can carry an auto-enqueue predecessor")
  892. check(wait_task_done(auth, predecessor, timeout=60) is not None,
  893. "the predecessor completes")
  894. check(wait_for(lambda: task_state(auth, successor)["status"] in
  895. ("queued", "assigned", "working") or False,
  896. timeout=30, what="successor auto-enqueues") is not None,
  897. "the done predecessor auto-enqueues its dependent")
  898. # Cycle validation refuses a predecessor chain that loops.
  899. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  900. "project_id": "1", "title": "Cycle A", "priority": "4"})
  901. cycle_a = task_id_of("Cycle A")
  902. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  903. "project_id": "1", "title": "Cycle B", "priority": "4",
  904. "enqueue_after_task_id": str(cycle_a)})
  905. cycle_b = task_id_of("Cycle B")
  906. cycle_directives = act(auth, task_state(auth, cycle_a)["update"], {
  907. "title": "Cycle A", "description": "x", "priority": "4",
  908. "enqueue_after_task_id": str(cycle_b)})
  909. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  910. and "cycle" in (d.get("message") or "").lower()
  911. for d in cycle_directives),
  912. "a predecessor cycle rejects with a naming error")
  913. # A failed predecessor leaves the dependent in draft.
  914. fake.fail_next = True
  915. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  916. "project_id": "1", "title": "Failing predecessor", "priority": "1"})
  917. failing_pred = task_id_of("Failing predecessor")
  918. enqueue_task(auth, failing_pred)
  919. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  920. "project_id": "1", "title": "Stranded successor", "priority": "4",
  921. "enqueue_after_task_id": str(failing_pred)})
  922. stranded = task_id_of("Stranded successor")
  923. check(wait_for(lambda: task_state(auth, failing_pred)["status"] == "failed"
  924. or False, timeout=60, what="failing predecessor fails") is not None,
  925. "the scripted-failure predecessor fails")
  926. check(quiet_window(quiet_seconds(),
  927. lambda: task_state(auth, stranded)["status"] == "draft",
  928. "failed predecessor leaves the draft"),
  929. "a failed predecessor leaves its dependent in draft")
  930. # Successor cleanup: let the queued successor finish so the
  931. # shard's app shuts down with an empty queue.
  932. check(wait_task_done(auth, successor, timeout=90) is not None,
  933. "the auto-enqueued successor completes")
  934. print("== feedback: context_update carries rule/note deltas on resume ==")
  935. # A standalone task parks on a question; while it is parked a rule
  936. # is attached to the project and a note lands; the resuming
  937. # assignment carries both as the context_update markdown turn.
  938. probe_id = create_queued(auth, {"project_id": "1", "title": "Context probe",
  939. "priority": "4"})
  940. check(wait_for(lambda: fake.running_task == probe_id
  941. and task_state(auth, probe_id)["status"] == "working" or False,
  942. timeout=60, what="probe task runs") is not None,
  943. "the probe task dispatches")
  944. fake.set_state("blocked")
  945. fake.push_results("blocked", block={"type": "question",
  946. "question": "Which database dialect?"})
  947. check(wait_for(lambda: task_state(auth, probe_id)["status"] == "blocked" or False,
  948. timeout=60, what="probe task blocks") is not None,
  949. "the probe task parks on its question")
  950. detail = task_state(auth, probe_id)
  951. open_block = [b for b in detail["blocks"] if not b["answered"]][0]
  952. # Backdate the parked round's start: stamps are second-resolution
  953. # epochs, and the delta compares strictly-later — the park must
  954. # span a real interval like an operator's would.
  955. conn = db()
  956. conn.execute("update sessions set started_at = strftime('%s','now') - 60"
  957. " where id = (select min(id) from sessions where task_id = ?)",
  958. (probe_id,))
  959. conn.commit()
  960. conn.close()
  961. # While parked: a new rule attached to the project + a new note.
  962. rules_state = slot_public(auth, "/rules", "rules")
  963. act(auth, rules_state["create"], {
  964. "name": "Nightly gate", "body": "Every PR must pass the nightly suite before merge."})
  965. conn = db()
  966. rule_id = conn.execute("select id from rules where name = 'Nightly gate'").fetchone()[0]
  967. conn.close()
  968. act(auth, slot_public(auth, "/projects/1", "project")["attach_rule"],
  969. {"rule_id": str(rule_id)})
  970. conn = db()
  971. conn.execute("insert into project_notes (project_id, body, created_at)"
  972. " values (1, 'The demo repo moved its CI to nightly builds.',"
  973. " strftime('%s','now'))")
  974. conn.commit()
  975. check(conn.execute("select count(*) from rule_projects where rule_id = ?",
  976. (rule_id,)).fetchone()[0] == 1,
  977. "the rule attaches to the project while the task is parked")
  978. conn.close()
  979. probe_assignments = len([p for p in fake.start_payloads
  980. if int(p.get("task", {}).get("id") or 0) == probe_id])
  981. act(auth, open_block["answer_action"], {"answer": "postgres"})
  982. check(wait_for(lambda: len([p for p in fake.start_payloads
  983. if int(p.get("task", {}).get("id") or 0) == probe_id])
  984. > probe_assignments or False, timeout=60,
  985. what="probe task resumes") is not None,
  986. "answering the block resumes the probe task")
  987. probe_resume = [p for p in fake.start_payloads
  988. if int(p.get("task", {}).get("id") or 0) == probe_id][-1]
  989. delta = probe_resume.get("context_update") or ""
  990. check("Rules changed" in delta and "Nightly gate" in delta
  991. and "nightly suite" in delta,
  992. "the resume's context_update summarises the newly attached rule")
  993. check("New project notes" in delta and "nightly builds" in delta,
  994. "the resume's context_update summarises the new project note")
  995. check("Plan approved" not in delta,
  996. "standalone resumes carry no plan section")
  997. check(wait_task_done(auth, probe_id, timeout=60) is not None,
  998. "the probe task completes after its resume")
  999. print("== goal 4 t1: force wake — the human resume for a parked agent ==")
  1000. # The goal-2 stall class: the parked run has NO pending wake (a
  1001. # draft task never enqueued → no terminal transition → parked
  1002. # forever). Force wake is the human recovery lever. A fresh goal
  1003. # owns the scenario (goal1's run completed with its goal).
  1004. stall_goal = create_goal({"project_id": "1", "title": "Stall goal",
  1005. "brief": "Parks with a never-enqueued draft.",
  1006. "branch_name": "goal/stall"})
  1007. fake.goal_script = [("plan", "# Stall plan\n\n1. Wait for the stall")]
  1008. act(auth, goal_state(stall_goal)["start"], {})
  1009. check(wait_for(lambda: goal_state(stall_goal)["status"] == "plan_review" or False,
  1010. timeout=60, what="stall plan enters review") is not None,
  1011. "the stall goal reaches plan review")
  1012. act(auth, goal_state(stall_goal)["plan_pending_block"]["answer_action"],
  1013. {"verdict": "accept"})
  1014. check(wait_for(lambda: goal_state(stall_goal)["status"] == "active" or False,
  1015. timeout=30, what="stall goal activates") is not None,
  1016. "the stall goal activates")
  1017. fake.goal_script = [("suspend",)]
  1018. conn = db()
  1019. run_task = conn.execute(
  1020. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  1021. (stall_goal,)).fetchone()[0]
  1022. conn.close()
  1023. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  1024. or False, timeout=60, what="agent parks for force wake") is not None,
  1025. "the goal agent parks before the force-wake scenario")
  1026. # The stall fixture: an agent-created draft that was never enqueued
  1027. # (typo'd enqueue) — statuses must name it as NOT enqueued.
  1028. conn = db()
  1029. agent_session = conn.execute(
  1030. "select max(id) from sessions where task_id = ?", (run_task,)).fetchone()[0]
  1031. conn.close()
  1032. status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
  1033. "session_id": agent_session, "title": "QA sweep",
  1034. "mandate": "Run the QA sweep.", "mode": "test"})
  1035. check(status == 200 and body.get("ok") is True,
  1036. "the stall fixture draft task creates (%s)" % body.get("error"))
  1037. stall_draft = int(body["task_id"])
  1038. goal_page = http_get_page(auth, "/goals/%d" % stall_goal)
  1039. check('stm-action="goal.force_wake"' in goal_page,
  1040. "the goal page authors the Force wake button while the run is parked")
  1041. check('stm-if="goal.force_wake != null"' in goal_page,
  1042. "the Force wake button is gated on the action being offered")
  1043. # No wake without the press: parked stays parked.
  1044. parked_runs = len(fake.goal_assignments)
  1045. check(quiet_window(quiet_seconds(),
  1046. lambda: len(fake.goal_assignments) == parked_runs,
  1047. "no press, no wake"),
  1048. "without a press the parked agent never wakes")
  1049. state = goal_state(stall_goal)
  1050. check(state.get("force_wake") is not None,
  1051. "the goal state exposes the offered force-wake action")
  1052. act(auth, state["force_wake"], {})
  1053. check(wait_for(lambda: len(fake.goal_assignments) > parked_runs or False,
  1054. timeout=30, what="force-woke agent resumes") is not None,
  1055. "the force-woke agent resumes immediately (no coalesce wait)")
  1056. woke_notice = fake.goal_assignments[-1].get("resume_notice") or ""
  1057. check("force-woke this goal's supervision" in woke_notice
  1058. and "no pending wake" in woke_notice,
  1059. "the force-wake resume leads with the explicit wake line")
  1060. check(("draft (NOT enqueued" in woke_notice)
  1061. and ("saddle task enqueue %d" % stall_draft) in woke_notice,
  1062. "the statuses list names the never-enqueued draft with the enqueue fix")
  1063. check("Task statuses" in woke_notice,
  1064. "the force-wake notice still carries the standard batched report")
  1065. # The flag is consumed: re-park (script), no second wake fires.
  1066. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  1067. or False, timeout=60, what="woken agent re-parks") is not None,
  1068. "the woken agent parks again (script)")
  1069. woke_runs = len(fake.goal_assignments)
  1070. check(quiet_window(quiet_seconds(),
  1071. lambda: len(fake.goal_assignments) == woke_runs,
  1072. "consumed flag never re-fires"),
  1073. "the consumed flag does not re-wake (one wake per press)")
  1074. # Guards: a paused goal refuses the wake (Resume is its lever).
  1075. state = goal_state(stall_goal)
  1076. act(auth, state["pause"], {})
  1077. conn = db()
  1078. paused_flag = conn.execute("select force_wake from goals where id = ?",
  1079. (stall_goal,)).fetchone()
  1080. conn.close()
  1081. check(paused_flag is not None and paused_flag[0] in (0, False),
  1082. "pausing does not set the flag")
  1083. state = goal_state(stall_goal)
  1084. check(state.get("force_wake") is None,
  1085. "a paused goal does not author the Force wake action")
  1086. act(auth, state["resume"], {})
  1087. check(wait_for(lambda: goal_state(stall_goal)["status"] == "active" or False,
  1088. timeout=30, what="goal reactivates") is not None,
  1089. "the goal reactivates for the guard checks")
  1090. # Non-suspended run refuses: run is parked again (script parked it
  1091. # above), wake it, let it park, then a second press while parked is
  1092. # fine — the 400 path needs a NON-suspended run. Start there: wake
  1093. # once more and press while it is working.
  1094. state = goal_state(stall_goal)
  1095. act(auth, state["force_wake"], {})
  1096. check(wait_for(lambda: len(fake.goal_assignments) > woke_runs or False,
  1097. timeout=30, what="second wake fires") is not None,
  1098. "a second press wakes again (re-press is allowed)")
  1099. # While it is mid-script (not yet parked), the action is hidden.
  1100. check(goal_state(stall_goal).get("force_wake") is None
  1101. or task_state(auth, run_task)["status"] != "suspended",
  1102. "the wake action hides while the run is not parked")
  1103. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
  1104. or False, timeout=60, what="run parks for cleanup") is not None,
  1105. "the run parks again")