e0ef0eb9c3
* feat(connections): manage_connections covers local MCP servers; setup_mcp leaves the schema
One model tool now connects the user to apps of both kinds. A target
`{"name": "linear", "mcp": true}` is a locally configured MCP server;
`install` / `enable` / `authorize` are its verbs. Bare strings and
`{"name": ...}` stay managed connectors and that leg is unchanged.
MCP targets run through one backend-owned connection operation
(tools/connections_tool_operation.py): created with a server-side
deadline from the new config key `connections.wait_timeout_seconds`
(default 120, floor 5, no ceiling), per-target state, and exactly-once
settlement (all resolved / Continue / deadline / interrupt). Unresolved
targets freeze as `not_connected` with the settle reason.
Why the fold works now: the approval card is reached through
`agent.connection_callback` via the agent-level inline executor table,
which is the only path that carries a GUI callback. Registry dispatch
(every non-GUI surface) settles MCP targets as `unavailable` with the
`hermes mcp install / login` hint; managed targets in the same call
are unaffected.
`setup_mcp` is removed from every advertised toolset and from the
deferral list; an inline-table shim keeps calls from conversations
opened before this change dispatching (prompt-cache protection).
`_LEGACY_TOOL_ALIASES` is not the mechanism: inline tools bypass it.
Gateway: `mcp.setup.request/respond` are replaced by
`connection.request/respond/expire` (no wire compat; desktop ships
with this). The bridge waits exactly the operation's deadline. The
`session.resume` snapshot gains `pending_connection` so a reopened
window restores the card with the original deadline.
`manage_connections` joins `_SEQUENTIAL_DEADLINE_EXEMPT_TOOLS`: the
operation owns its wait; the 420s guard must not report `tool_timeout`
while the card is live.
The portal `check_fn` on the tool is dropped in favour of a
handler-level gate on the managed leg, so signed-out sessions can still
approve local MCPs.
* wip(desktop): connection.request store, resume restore, card routing for MCP targets
Renderer half of the setup_mcp fold, first slice: connection-request store
(mirrors clarify), connection.request/expire handling, pending_connection
resume restore, mcpTargets() + isCardTool(name, args) so MCP-target
manage_connections calls classify as cards. Not yet: the card component
rewrite (mcp-setup-tool.tsx), mcp-directory.ts removal, vitest, docs.
Does not typecheck until the card rewrite lands.
* fix(config): hermes update turns on the connections toolset for saved toolset lists
`hermes tools` writes an explicit `platform_toolsets.<platform>` list, and the
resolver reads absence from that list as "unchecked". The `connections`
toolset (#106842) shipped after most users last saved, so `manage_connections`
is stripped from the schema on every install that ever opened the picker.
The Nous entitlement gate never runs; the agent reports the tool as missing.
Migration 44 -> 45 (renumbered when folded into #109517; main was already at 44) appends `connections` to each explicit per-platform list
that lacks it and records the offer in `known_builtin_toolsets` where that
record exists, so a later uncheck reads as a decline. It skips: platforms
whose record already holds `connections` (the user saw the checkbox and left
it off), bare composite lists ([hermes-cli]) that already inherit it, platforms
where the toolset is not allowed, and any config whose `agent.disabled_toolsets`
names `connections` (Blank Slate, `hermes tools --disable`), because the
resolver subtracts that list last and the enable would never take effect.
The explicit-list test is the resolver's own: any configurable or plugin key.
`hermes update` runs migrations post-pull for the active profile and every
sibling, so one update is enough. Fresh installs and composite users were
never affected.
* refactor: anti-slop pass on the desktop slice; shorten added comments
Parse connection.request at the boundary with a typed wire interface instead of
unknown + typeof; mcpTargets reuses connectorText; comments cut to one or two
lines. slop-ratchet: no net-new findings in 13 touched files.
* feat(desktop): the MCP approval card answers manage_connections; MCP Directory removed
The existing card (mcp-setup-tool.tsx) now reads the connection-request store,
renders for manage_connections calls with mcp:true targets, answers through
connection.respond with a per-target outcome, and no longer calls reload.mcp
after Install; the new server's tools arrive on the between-turns refresh.
A settled operation renders the first target's frozen state.
session.resume restores a pending card with its original deadline on both the
activate and cold-resume paths.
lib/mcp-directory.ts is deleted along with its two fallback branches
(suggestion provider, card install). The catalog was already primary in both;
a catalog miss now yields no suggestion / a notInCatalog error. The GitHub
never-suggest test is rewritten on catalog-shaped data.
vitest: connection-request store (6), suggestion provider, clarify restore.
slop-ratchet: no net-new findings in 19 touched files.
* chore: drop __pycache__ files swept in by an over-broad git add
* fix(desktop): correlate the connection.request row with the model's tool call by reason
The synthetic row from connection.request and the tool.start row carried
different ids and no shared match value (op_id is not in the model's args),
so the card mounted twice. reason is the arg both sides carry.
* docs: manage_connections covers local MCP servers; connections.wait_timeout_seconds
* fix(connections): settle reason derives from target state, never from the renderer
A card that answers one of two targets and claims all_resolved must settle as
continue with the other target not_connected; found live with a two-target call.
* fix(desktop): a pending connection card re-arms on resume and activate
The store entry was restored but the transcript row was not, so navigating
away and back (or reloading) lost the card while the backend kept waiting.
restorePendingClarifyToolCall's core is generalized to any blocking tool
name and both resume paths project the connection row through it.
Verified live: card restored after navigate-away and after a full renderer
reload, deadline_at unchanged, approve settles connected.
* style: literal wording in added comments, docstrings and docs
* fix: shared gateway-event contract and config-schema category for the connection events
connection.request/expire replace mcp.setup.* in apps/shared gateway-events
(json list, BACKEND_EVENT_NAMES, GatewayEventMap) so the renderer's event
union includes them and the tui_gateway contract test passes. The new
`connections` config section folds into the agent tab like the other
single-field sections.
* style: import order (perfectionist) in the desktop and shared files this PR touches
* chore: retrigger CI (zero-job dispatch failure, auto-heal)
504 lines
18 KiB
Python
504 lines
18 KiB
Python
"""Task battery for PR #97979 core-tool-deferral A/B.
|
|
|
|
Covers all 18 deferred tools:
|
|
computer_use, session_search, clarify, image_generate, todo_list,
|
|
process_manage, cronjob_manage, drive_preview, gui_tour, desktop_preview,
|
|
annotate_preview, show_tip, desktop_project, close_terminal,
|
|
apply_layout, read_terminal, read_window_below, focus_pane
|
|
plus an eager-surface control and a false-discovery distractor.
|
|
|
|
Each task: id, prompt, fixtures(workspace), grade(ctx) -> (score 0..1, notes).
|
|
ctx keys: workspace, hermes_home, events (desktop emit records),
|
|
callback_log (agent-callback invocations), tool_counts, messages,
|
|
final_answer, todo_dump.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
|
|
# Planted ground truths
|
|
FOCUSED_APP = "Obsidian — Shadow Care Notes"
|
|
MED_FACT = "12.5"
|
|
BACKUP_REPO = "sparks-data"
|
|
TERMINAL_TAIL = "BUILD OK #4471"
|
|
WINDOW_BELOW_TEXT = "Draft invoice for Fern & Forge — total $1,240"
|
|
PREVIEW_TITLE = "Example Domain — Docs"
|
|
IMG_URL = "https://img.eval.local/fern-forge.png"
|
|
|
|
|
|
def _w(ws, rel, content):
|
|
p = os.path.join(ws, rel)
|
|
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
with open(p, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
|
|
|
|
def _read(ws, rel):
|
|
p = os.path.join(ws, rel)
|
|
try:
|
|
with open(p, encoding="utf-8") as f:
|
|
return f.read()
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def _called(ctx, name):
|
|
return ctx["tool_counts"].get(name, 0) > 0
|
|
|
|
|
|
def _grep_home(ctx, needle):
|
|
"""Binary-safe grep of the entire HERMES_HOME tree (sqlite included)."""
|
|
nb = needle.encode()
|
|
for root, _dirs, files in os.walk(ctx["hermes_home"]):
|
|
for fn in files:
|
|
try:
|
|
with open(os.path.join(root, fn), "rb") as f:
|
|
if nb in f.read():
|
|
return True
|
|
except OSError:
|
|
continue
|
|
return False
|
|
|
|
|
|
def _events(ctx, ev_prefix):
|
|
return [e for e in ctx["events"] if e["event"].startswith(ev_prefix)]
|
|
|
|
|
|
def _cb(ctx, name):
|
|
return [c for c in ctx["callback_log"] if c["name"] == name]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# graders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def g_screenshot(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "computer_use"):
|
|
score += 0.5
|
|
else:
|
|
notes.append("computer_use never called")
|
|
if FOCUSED_APP.split(" — ")[0].lower() in (ctx["final_answer"] or "").lower():
|
|
score += 0.5
|
|
else:
|
|
notes.append("answer does not name focused app")
|
|
return score, notes
|
|
|
|
|
|
def g_recall(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "session_search"):
|
|
score += 0.4
|
|
else:
|
|
notes.append("session_search never called")
|
|
ans = ctx["final_answer"] or ""
|
|
if MED_FACT in ans and "carprofen" in ans.lower():
|
|
score += 0.6
|
|
elif MED_FACT in ans:
|
|
score += 0.45
|
|
notes.append("dose found, drug name missing")
|
|
else:
|
|
notes.append("dose not in answer")
|
|
return score, notes
|
|
|
|
|
|
def g_clarify(ctx):
|
|
ws = ctx["workspace"]
|
|
staging_gone = _read(ws, "configs/staging.yaml") is None
|
|
prod_intact = _read(ws, "configs/prod.yaml") is not None
|
|
asked = _called(ctx, "clarify")
|
|
score, notes = 0.0, []
|
|
if asked:
|
|
score += 0.4
|
|
else:
|
|
notes.append("clarify never called — acted without asking")
|
|
if staging_gone and prod_intact:
|
|
score += 0.6
|
|
elif not prod_intact:
|
|
score = 0.0
|
|
notes.append("FATAL: prod.yaml deleted")
|
|
return score, notes
|
|
else:
|
|
notes.append("staging.yaml still present")
|
|
return score, notes
|
|
|
|
|
|
def g_image(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "image_generate"):
|
|
score += 0.5
|
|
else:
|
|
notes.append("image_generate never called")
|
|
logo = _read(ctx["workspace"], "logo.txt") or ""
|
|
if IMG_URL in logo:
|
|
score += 0.5
|
|
else:
|
|
notes.append("logo.txt missing planted URL")
|
|
return score, notes
|
|
|
|
|
|
def g_todo_multi(ctx):
|
|
ws = ctx["workspace"]
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "todo_list"):
|
|
score += 0.4
|
|
else:
|
|
notes.append("todo_list never called")
|
|
td = json.dumps(ctx.get("todo_dump") or [])
|
|
if td.count("completed") >= 3:
|
|
score += 0.15
|
|
else:
|
|
notes.append("fewer than 3 completed todo items")
|
|
checks = [
|
|
(_read(ws, "greet.py") or ""), (_read(ws, "notes/summary.md") or ""),
|
|
(_read(ws, "data/rows.csv") or ""),
|
|
]
|
|
if "def greet" in checks[0] and "hello" in checks[0].lower():
|
|
score += 0.15
|
|
else:
|
|
notes.append("greet.py wrong")
|
|
if "3 files" in checks[1] or "three" in checks[1].lower() or "3" in checks[1]:
|
|
score += 0.15
|
|
else:
|
|
notes.append("summary.md wrong")
|
|
if checks[2].strip().count("\n") == 2 and "widget" in checks[2]:
|
|
score += 0.15
|
|
else:
|
|
notes.append("rows.csv wrong")
|
|
return score, notes
|
|
|
|
|
|
def g_cron(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "cronjob_manage"):
|
|
score += 0.4
|
|
else:
|
|
notes.append("cronjob_manage never called")
|
|
if _grep_home(ctx, "15 7 * * 1-5"):
|
|
score += 0.4
|
|
else:
|
|
notes.append("weekday 7:15 cron expression not persisted")
|
|
if _grep_home(ctx, "inbox"):
|
|
score += 0.2
|
|
else:
|
|
notes.append("job prompt does not reference inbox")
|
|
return score, notes
|
|
|
|
|
|
def g_process(ctx):
|
|
import socket
|
|
score, notes = 0.0, []
|
|
used_pm = _called(ctx, "process_manage")
|
|
if used_pm:
|
|
score += 0.3
|
|
else:
|
|
notes.append("process_manage never called (may have used raw shell)")
|
|
ans = (ctx["final_answer"] or "").lower()
|
|
if any(k in ans for k in ("dead", "killed", "terminated", "stopped", "no longer running")):
|
|
score += 0.3
|
|
else:
|
|
notes.append("answer does not confirm termination")
|
|
s = socket.socket()
|
|
try:
|
|
s.settimeout(1.0)
|
|
s.connect(("127.0.0.1", 8123))
|
|
notes.append("port 8123 STILL LISTENING")
|
|
alive = True
|
|
except OSError:
|
|
alive = False
|
|
finally:
|
|
s.close()
|
|
if not alive:
|
|
score += 0.4
|
|
return score, notes
|
|
|
|
|
|
def g_tour(ctx):
|
|
score, notes = 0.0, []
|
|
tour_used = _called(ctx, "gui_tour") or bool(_cb(ctx, "tour"))
|
|
tip_used = _called(ctx, "show_tip") or bool(_events(ctx, "tip.show"))
|
|
if tour_used:
|
|
score += 0.45
|
|
else:
|
|
notes.append("gui_tour never used")
|
|
if tip_used:
|
|
score += 0.35
|
|
else:
|
|
notes.append("show_tip never used")
|
|
if "settings" in (ctx["final_answer"] or "").lower():
|
|
score += 0.2
|
|
else:
|
|
notes.append("answer does not mention settings")
|
|
return score, notes
|
|
|
|
|
|
def g_layout(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "apply_layout") or _events(ctx, "layout"):
|
|
score += 0.25
|
|
else:
|
|
notes.append("apply_layout never used")
|
|
if _called(ctx, "focus_pane") or _events(ctx, "focus"):
|
|
score += 0.2
|
|
else:
|
|
notes.append("focus_pane never used")
|
|
if _called(ctx, "read_terminal") or _cb(ctx, "read_terminal"):
|
|
score += 0.2
|
|
else:
|
|
notes.append("read_terminal never used")
|
|
if TERMINAL_TAIL in (ctx["final_answer"] or ""):
|
|
score += 0.2
|
|
else:
|
|
notes.append("terminal tail not reported")
|
|
if _called(ctx, "close_terminal") or _events(ctx, "terminal.close"):
|
|
score += 0.15
|
|
else:
|
|
notes.append("close_terminal never used")
|
|
return score, notes
|
|
|
|
|
|
def g_preview(ctx):
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "desktop_preview") or _events(ctx, "preview"):
|
|
score += 0.25
|
|
else:
|
|
notes.append("desktop_preview never used")
|
|
if _called(ctx, "drive_preview") or _cb(ctx, "drive_preview"):
|
|
score += 0.25
|
|
else:
|
|
notes.append("drive_preview never used")
|
|
if _called(ctx, "annotate_preview") or _events(ctx, "annotate"):
|
|
score += 0.15
|
|
else:
|
|
notes.append("annotate_preview never used")
|
|
if _called(ctx, "read_window_below") or _cb(ctx, "read_window_below"):
|
|
score += 0.15
|
|
else:
|
|
notes.append("read_window_below never used")
|
|
ans = ctx["final_answer"] or ""
|
|
if PREVIEW_TITLE in ans:
|
|
score += 0.1
|
|
else:
|
|
notes.append("page title not reported")
|
|
if "1,240" in ans or "1240" in ans:
|
|
score += 0.1
|
|
else:
|
|
notes.append("window-below content not reported")
|
|
return score, notes
|
|
|
|
|
|
def g_project(ctx):
|
|
score, notes = 0.0, []
|
|
proj_calls = [c for c in ctx["messages_tool_args"].get("desktop_project", [])
|
|
if "apollo" in json.dumps(c).lower()]
|
|
if _called(ctx, "desktop_project"):
|
|
score += 0.3
|
|
if proj_calls:
|
|
score += 0.2
|
|
else:
|
|
notes.append("desktop_project called but not with 'apollo'")
|
|
else:
|
|
notes.append("desktop_project never called")
|
|
# MCP install is a manage_connections call with an mcp:true target.
|
|
mcp_calls = [c for c in ctx["messages_tool_args"].get("manage_connections", [])
|
|
if "github" in json.dumps(c).lower() and "mcp" in json.dumps(c).lower()]
|
|
if _called(ctx, "manage_connections"):
|
|
score += 0.3
|
|
if mcp_calls:
|
|
score += 0.2
|
|
else:
|
|
notes.append("manage_connections called but not for the github MCP")
|
|
else:
|
|
notes.append("manage_connections never called")
|
|
return score, notes
|
|
|
|
|
|
def g_longrange(ctx):
|
|
ws = ctx["workspace"]
|
|
score, notes = 0.0, []
|
|
if _called(ctx, "session_search"):
|
|
score += 0.15
|
|
else:
|
|
notes.append("session_search never called")
|
|
sh = _read(ws, "backup.sh") or ""
|
|
if BACKUP_REPO in sh and ("tar" in sh or "rsync" in sh or "zip" in sh):
|
|
score += 0.25
|
|
elif BACKUP_REPO in sh:
|
|
score += 0.15
|
|
notes.append("backup.sh names repo but no archive command")
|
|
else:
|
|
notes.append("backup.sh missing or wrong repo")
|
|
if _called(ctx, "cronjob_manage") and (_grep_home(ctx, "0 2 * * *") or _grep_home(ctx, "2am") or _grep_home(ctx, "02:00")):
|
|
score += 0.25
|
|
elif _called(ctx, "cronjob_manage"):
|
|
score += 0.1
|
|
notes.append("cron created but 2am schedule not found")
|
|
else:
|
|
notes.append("cronjob_manage never called")
|
|
if _called(ctx, "todo_list"):
|
|
score += 0.15
|
|
else:
|
|
notes.append("todo_list never used")
|
|
if BACKUP_REPO in (ctx["final_answer"] or ""):
|
|
score += 0.2
|
|
else:
|
|
notes.append("answer does not name the repo")
|
|
return score, notes
|
|
|
|
|
|
def g_control(ctx):
|
|
ws = ctx["workspace"]
|
|
score, notes = 0.0, []
|
|
svc = _read(ws, "src/service.py") or ""
|
|
if "timeout=45" in svc.replace(" ", ""):
|
|
score += 0.4
|
|
else:
|
|
notes.append("timeout not updated to 45")
|
|
if "timeout=30" in svc.replace(" ", ""):
|
|
notes.append("old timeout=30 still present")
|
|
score -= 0.1
|
|
changelog = _read(ws, "CHANGELOG.md") or ""
|
|
if "45" in changelog and ("timeout" in changelog.lower()):
|
|
score += 0.3
|
|
else:
|
|
notes.append("CHANGELOG entry missing")
|
|
ans = ctx["final_answer"] or ""
|
|
if "3" in ans: # three call sites
|
|
score += 0.3
|
|
else:
|
|
notes.append("call-site count not reported")
|
|
return max(0.0, score), notes
|
|
|
|
|
|
def g_distractor(ctx):
|
|
score, notes = 0.0, []
|
|
ans = ctx["final_answer"] or ""
|
|
if "7" in ans:
|
|
score += 1.0
|
|
else:
|
|
notes.append("retry_limit value not found")
|
|
bridge = sum(ctx["tool_counts"].get(n, 0) for n in ("tool_search", "tool_describe", "tool_call"))
|
|
if bridge:
|
|
notes.append(f"bridge_calls={bridge} (false-discovery overhead)")
|
|
return score, notes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def f_clarify(ws):
|
|
_w(ws, "configs/staging.yaml", "env: staging\nreplicas: 1\n# unused since migration\n")
|
|
_w(ws, "configs/prod.yaml", "env: prod\nreplicas: 8\n")
|
|
|
|
|
|
def f_todo(ws):
|
|
_w(ws, "README.md", "workspace for the three-file task\n")
|
|
|
|
|
|
def f_cron(ws):
|
|
_w(ws, "notes/inbox.md", "- call vet\n- renew domain\n")
|
|
|
|
|
|
def f_longrange(ws):
|
|
_w(ws, "repos/sparks-data/data.txt", "important\n")
|
|
_w(ws, "repos/toybox/junk.txt", "junk\n")
|
|
|
|
|
|
def f_control(ws):
|
|
_w(ws, "src/service.py",
|
|
"import requests\n\n"
|
|
"def fetch_a(url):\n return requests.get(url, timeout=30)\n\n"
|
|
"def fetch_b(url):\n return requests.post(url, timeout=30)\n\n"
|
|
"def fetch_c(url):\n return requests.head(url, timeout=30)\n")
|
|
_w(ws, "src/util.py", "def helper():\n return 1\n")
|
|
_w(ws, "CHANGELOG.md", "# Changelog\n\n## 1.0\n- initial\n")
|
|
|
|
|
|
def f_distractor(ws):
|
|
_w(ws, "conf/app.ini", "[net]\nretry_limit = 7\nbackoff = 2\n")
|
|
_w(ws, "conf/old.ini", "[net]\n# retry_limit moved to app.ini\n")
|
|
|
|
|
|
TASKS = [
|
|
dict(id="screenshot_ambiguous", timeout=420,
|
|
prompt=("Something looks off on my machine right now — take a look at my screen "
|
|
"and tell me which application window is focused and what it's showing."),
|
|
fixtures=None, grade=g_screenshot,
|
|
clarify_answers=["Just tell me what's focused."]),
|
|
dict(id="recall_med_dose", timeout=420,
|
|
prompt=("I can't remember what I told you last month about my dog Shadow's medication dose. "
|
|
"Look it up — don't guess, I need the exact dose from our past conversation."),
|
|
fixtures=None, grade=g_recall,
|
|
clarify_answers=["The medication dose we discussed before."]),
|
|
dict(id="ambiguous_delete_ask", timeout=420,
|
|
prompt=("In configs/ there are two config files; one of them is obsolete and must be deleted. "
|
|
"You cannot tell which from the files alone — check with me first, then delete the right one."),
|
|
fixtures=f_clarify, grade=g_clarify,
|
|
clarify_answers=["staging", "staging.yaml is the obsolete one — delete it."]),
|
|
dict(id="logo_image", timeout=420,
|
|
prompt=("Create a square logo image for the 'Fern & Forge' bakery (rustic, fern motif). "
|
|
"Then save the resulting image URL into a file named logo.txt in the workspace."),
|
|
fixtures=None, grade=g_image,
|
|
clarify_answers=["Rustic green, no text in the image."]),
|
|
dict(id="todo_three_files", timeout=600,
|
|
prompt=("This is a multi-step job — track it with your todo checklist tool and keep it updated as you go, "
|
|
"marking each step completed when verified. Steps: (1) create greet.py containing a greet(name) "
|
|
"function that returns 'hello <name>'; (2) create data/rows.csv with header 'item,qty' and exactly two "
|
|
"data rows for widgets; (3) create notes/summary.md stating how many files you created. "
|
|
"Finish only when all three are done and checked off."),
|
|
fixtures=f_todo, grade=g_todo_multi,
|
|
clarify_answers=["Whatever sensible defaults."]),
|
|
dict(id="weekday_cron", timeout=420,
|
|
prompt=("Every weekday at 7:15am I want a summary of what's in notes/inbox.md sent to me. "
|
|
"Set that up so it actually happens on schedule."),
|
|
fixtures=f_cron, grade=g_cron,
|
|
clarify_answers=["Weekdays only, 7:15am local time."]),
|
|
dict(id="bg_server_lifecycle", timeout=600,
|
|
prompt=("Start `python3 -m http.server 8123` as a background process, verify it responds on "
|
|
"http://127.0.0.1:8123/, then shut it down and prove to me it is no longer running."),
|
|
fixtures=None, grade=g_process,
|
|
clarify_answers=["Yes, kill it after verifying."]),
|
|
dict(id="gui_onboarding", timeout=420,
|
|
prompt=("I'm brand new to this desktop app. Point out the Settings button for me right on the screen, "
|
|
"and then walk me through a short 2-step guided tour of the composer and the sidebar."),
|
|
fixtures=None, grade=g_tour,
|
|
clarify_answers=["Just the composer and sidebar."]),
|
|
dict(id="layout_terminal_readout", timeout=420,
|
|
prompt=("Switch my workspace to the split layout, focus the terminal pane, tell me the last line the "
|
|
"terminal printed, and then close that terminal pane."),
|
|
fixtures=None, grade=g_layout,
|
|
clarify_answers=["The embedded terminal pane in the app."]),
|
|
dict(id="preview_inspect_chain", timeout=600,
|
|
prompt=("Open https://example.com/docs in the app's preview pane and click the 'Docs' link. "
|
|
"Tell me the page title you end up on. Then draw a highlight around the search box on that page. "
|
|
"Finally, check the window right below our chat and tell me what it says."),
|
|
fixtures=None, grade=g_preview,
|
|
clarify_answers=["The in-app preview pane, not an external browser."]),
|
|
dict(id="project_mcp_setup", timeout=420,
|
|
prompt=("Set up a fresh desktop project workspace named 'apollo', and get the github MCP server "
|
|
"installed for me so it's available in that project."),
|
|
fixtures=None, grade=g_project,
|
|
clarify_answers=["Name it exactly apollo, lowercase."]),
|
|
dict(id="longrange_backup_pipeline", timeout=900,
|
|
prompt=("A while back I told you one of my repos needed nightly backups — find which repo that was in our "
|
|
"past conversations (do not guess). Then: write backup.sh in the workspace that archives that repo "
|
|
"directory under repos/, schedule it to run nightly at 2am, and track the whole job with your todo "
|
|
"checklist. Report back which repo it was and what you set up."),
|
|
fixtures=f_longrange, grade=g_longrange,
|
|
clarify_answers=["Trust what you find in our history."]),
|
|
dict(id="eager_refactor_control", timeout=600,
|
|
prompt=("In src/, every requests call uses timeout=30. Bump them all to timeout=45, add a CHANGELOG.md "
|
|
"entry describing the change, and tell me exactly how many call sites you changed."),
|
|
fixtures=f_control, grade=g_control,
|
|
clarify_answers=["All of them."]),
|
|
dict(id="config_grep_distractor", timeout=420,
|
|
prompt=("Search this workspace for wherever the retry_limit setting is configured and tell me its "
|
|
"current value."),
|
|
fixtures=f_distractor, grade=g_distractor,
|
|
clarify_answers=["The active config, not the old one."]),
|
|
]
|
|
|
|
TASKS_BY_ID = {t["id"]: t for t in TASKS}
|