@widgex/wgx 0.0.0-stage → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.txt +49 -0
  2. package/README.md +33 -2
  3. package/ThirdPartyNotices.txt +4685 -0
  4. package/package.json +30 -4
  5. package/provider-clis.json +1211 -0
  6. package/python/XACTOR.md +146 -0
  7. package/python/browser_actor_host.py +456 -0
  8. package/python/cdp_page.py +363 -0
  9. package/python/harness_check.py +77 -0
  10. package/python/requirements.txt +1 -0
  11. package/python/test_line_endings.py +185 -0
  12. package/python/xactor/__init__.py +18 -0
  13. package/python/xactor/__main__.py +226 -0
  14. package/python/xactor/api.py +882 -0
  15. package/python/xactor/bridge.py +784 -0
  16. package/python/xactor/dom.py +117 -0
  17. package/python/xactor/man/add_card.md +89 -0
  18. package/python/xactor/man/agent-vs-fork.md +108 -0
  19. package/python/xactor/man/agent.md +333 -0
  20. package/python/xactor/man/ask_model.md +110 -0
  21. package/python/xactor/man/ask_reference.md +56 -0
  22. package/python/xactor/man/ask_user.md +103 -0
  23. package/python/xactor/man/attach_app.md +595 -0
  24. package/python/xactor/man/attach_ide.md +65 -0
  25. package/python/xactor/man/attach_ide_surface.md +31 -0
  26. package/python/xactor/man/await_user.md +214 -0
  27. package/python/xactor/man/bash.md +142 -0
  28. package/python/xactor/man/browser-driver.md +137 -0
  29. package/python/xactor/man/browser_activity.md +53 -0
  30. package/python/xactor/man/browser_attach.md +84 -0
  31. package/python/xactor/man/browser_close_tab.md +50 -0
  32. package/python/xactor/man/browser_detach.md +50 -0
  33. package/python/xactor/man/browser_exec.md +66 -0
  34. package/python/xactor/man/browser_focus_tab.md +47 -0
  35. package/python/xactor/man/browser_hybrid.md +47 -0
  36. package/python/xactor/man/browser_open.md +50 -0
  37. package/python/xactor/man/browser_open_tab.md +58 -0
  38. package/python/xactor/man/browser_plumbing.md +107 -0
  39. package/python/xactor/man/browser_screenshot.md +56 -0
  40. package/python/xactor/man/browser_snapshot.md +52 -0
  41. package/python/xactor/man/browser_tabs.md +67 -0
  42. package/python/xactor/man/bulletin.md +88 -0
  43. package/python/xactor/man/capture_page.md +54 -0
  44. package/python/xactor/man/cd.md +64 -0
  45. package/python/xactor/man/chess_move.md +53 -0
  46. package/python/xactor/man/children.md +89 -0
  47. package/python/xactor/man/close_task.md +14 -0
  48. package/python/xactor/man/compact.md +75 -0
  49. package/python/xactor/man/drive.md +140 -0
  50. package/python/xactor/man/drive_space.md +72 -0
  51. package/python/xactor/man/driving.md +168 -0
  52. package/python/xactor/man/edit_file.md +112 -0
  53. package/python/xactor/man/finding.md +194 -0
  54. package/python/xactor/man/fork_agent.md +45 -0
  55. package/python/xactor/man/glob.md +83 -0
  56. package/python/xactor/man/grep.md +115 -0
  57. package/python/xactor/man/harness.md +165 -0
  58. package/python/xactor/man/harness_check.md +70 -0
  59. package/python/xactor/man/harness_format.md +257 -0
  60. package/python/xactor/man/highlight.md +66 -0
  61. package/python/xactor/man/instruct.md +111 -0
  62. package/python/xactor/man/ls.md +63 -0
  63. package/python/xactor/man/man.md +86 -0
  64. package/python/xactor/man/merge_wiz.md +40 -0
  65. package/python/xactor/man/notebook.md +98 -0
  66. package/python/xactor/man/overview.md +89 -0
  67. package/python/xactor/man/permissions.md +137 -0
  68. package/python/xactor/man/phase.md +150 -0
  69. package/python/xactor/man/powershell.md +74 -0
  70. package/python/xactor/man/profiles.md +60 -0
  71. package/python/xactor/man/project_drive.md +49 -0
  72. package/python/xactor/man/read_bulletin.md +89 -0
  73. package/python/xactor/man/read_file.md +90 -0
  74. package/python/xactor/man/relay.md +216 -0
  75. package/python/xactor/man/reply.md +72 -0
  76. package/python/xactor/man/reply_message.md +78 -0
  77. package/python/xactor/man/report_bug.md +118 -0
  78. package/python/xactor/man/request_answer.md +142 -0
  79. package/python/xactor/man/request_merge_decisions.md +121 -0
  80. package/python/xactor/man/request_operator_action.md +152 -0
  81. package/python/xactor/man/request_review.md +122 -0
  82. package/python/xactor/man/request_user_action.md +126 -0
  83. package/python/xactor/man/return_card.md +71 -0
  84. package/python/xactor/man/run.md +99 -0
  85. package/python/xactor/man/sandbox.md +144 -0
  86. package/python/xactor/man/session_fork.md +74 -0
  87. package/python/xactor/man/session_harness.md +109 -0
  88. package/python/xactor/man/session_join.md +102 -0
  89. package/python/xactor/man/session_land.md +73 -0
  90. package/python/xactor/man/session_nudge.md +73 -0
  91. package/python/xactor/man/session_rest.md +73 -0
  92. package/python/xactor/man/session_scope.md +84 -0
  93. package/python/xactor/man/session_terminate.md +58 -0
  94. package/python/xactor/man/session_token.md +78 -0
  95. package/python/xactor/man/sessions.md +93 -0
  96. package/python/xactor/man/set_harness.md +15 -0
  97. package/python/xactor/man/spawn_swarm.md +80 -0
  98. package/python/xactor/man/submit_evidence.md +54 -0
  99. package/python/xactor/man/swarm_brief.md +30 -0
  100. package/python/xactor/man/task_status.md +82 -0
  101. package/python/xactor/man/tasks.md +47 -0
  102. package/python/xactor/man/todo_write.md +104 -0
  103. package/python/xactor/man/update_card.md +70 -0
  104. package/python/xactor/man/use_profile.md +90 -0
  105. package/python/xactor/man/use_widget.md +82 -0
  106. package/python/xactor/man/wait_for_task.md +105 -0
  107. package/python/xactor/man/widget-model.md +284 -0
  108. package/python/xactor/man/widgets.md +59 -0
  109. package/python/xactor/man/write_file.md +76 -0
  110. package/python/xactor/man/writing-a-widget.md +142 -0
  111. package/python/xactor/man/xylon.md +120 -0
  112. package/python/xactor/man/xylon_blame.md +80 -0
  113. package/python/xactor/man/xylon_read.md +131 -0
  114. package/python/xactor/man/xylon_search.md +125 -0
  115. package/python/xactor/manual.py +195 -0
  116. package/python/xactor/parent_watch.py +115 -0
  117. package/python/xactor/plan_mode.py +350 -0
  118. package/python/xactor/sandbox.py +856 -0
  119. package/python/xactor/seals.py +438 -0
  120. package/python/xactor/shell_hints.py +210 -0
  121. package/python/xactor/states/__init__.py +13 -0
  122. package/python/xactor/states/base.py +523 -0
  123. package/python/xactor/states/registry.py +274 -0
  124. package/python/xactor/stdlib_api.py +6345 -0
  125. package/python/xactor/widgets/__init__.py +1 -0
  126. package/python/xactor/widgets/github_tokens.py +314 -0
  127. package/python/xactor/widgets/google_aistudio.py +365 -0
  128. package/settings-schema.json +403 -0
  129. package/viewer/index.css +160 -0
  130. package/viewer/index.js +15 -0
  131. package/wgx.mjs +290659 -0
@@ -0,0 +1,216 @@
1
+ # Relay — send / inbox / connect / close
2
+ <!-- also: relay_connect, relay_send, relay_inbox, relay_accept, relay_decline, relay_close -->
3
+ <!-- group: Talking to other agents -->
4
+
5
+ Message another session, on this machine or another one. None is gated.
6
+
7
+ ## Two kinds of relay
8
+
9
+ **A message** — `relay_send(node, subject, body)` to any session. There is no
10
+ channel to open and nothing for the other side to accept: it is in their inbox
11
+ as soon as their machine has read it, they are told once, and they read it and
12
+ answer — or not — when they choose to.
13
+
14
+ **Delivered is not read, and does not start a turn.** If the recipient is busy it
15
+ sees the notice at its next tool result. If it is **idle**, a person at its
16
+ machine is asked whether to wake it (a session running Autonomous is woken
17
+ without asking, unless the message came from another account). Until someone
18
+ answers, or opens that chat, it stays idle with the message sitting in its
19
+ inbox — `relay_inbox()["sent"]` still says `delivered`, and for a session on
20
+ another machine a message can sit at `waiting` for the same reason. Neither
21
+ status says anyone is working on it. If you need an answer from an idle session
22
+ on someone else's machine, tell its person out of band to look for the wake
23
+ prompt.
24
+
25
+ **Forced** — orchestration, and nothing else. A parent may take a channel on a
26
+ **direct child** without asking it, one at a time, with
27
+ `relay_connect(node, force=True)`. The child does not consent and never reads an
28
+ inbox: your message arrives as its prompt, and your `relay_send` **blocks** until
29
+ its turn ends and returns what it said. You are its user, until you close it.
30
+
31
+ Reach for `force=True` when you spawned a session to do a job and you are the
32
+ one deciding what it does next. For anything else, send a message.
33
+
34
+ ## How a message travels
35
+
36
+ It is a line in **your own transcript**. That is the whole of sending: the call
37
+ writes it and returns. Whichever machine is running the other session delivers
38
+ it — by appending it to *their* transcript — once it has read yours:
39
+
40
+ - **In this window** — delivered before `relay_send` returns: `status: 'delivered'`.
41
+ - **Another window here, or a machine the sync is linked to** — a few seconds.
42
+ - **A machine that is offline** — when it next syncs. Nothing has to be online
43
+ at the same time; the message waits in your transcript.
44
+ - **A session that is not running anywhere** — when it next runs.
45
+
46
+ You find out it arrived the same way: their transcript comes back with it in.
47
+ `relay_inbox()["sent"]` marks each of your messages `delivered` or `waiting`.
48
+ **Do not send it again** because it says `waiting` — it is already on its way,
49
+ and a second send is a second message.
50
+
51
+ A private task's transcript never leaves this machine, so a message from one
52
+ reaches only sessions here. The return says so when that is the case.
53
+
54
+ ## Naming the other session
55
+
56
+ Every call takes a session the way notices and the bulletin print it:
57
+ `"fix-auth/session3"`. A session on another machine is named the same way, once
58
+ its transcript has synced here. `"."` is you and `".."` is your parent. A full
59
+ `repo/issue/session` and a bare session id also work. The older
60
+ `"@machine/issue/session"` form still resolves — the machine part is ignored,
61
+ because a session is the same session wherever it runs.
62
+
63
+ If the session id at the end of a name is one this machine holds, it resolves
64
+ even when the issue folder in front of it is spelled differently here (`-2`
65
+ suffixes appear when two folders collide). A task that has synced without any
66
+ session transcript in it has nothing to address — ask its owner for the session id.
67
+
68
+ A name that matches nothing, or more than one session, **raises** with who you
69
+ could have meant.
70
+
71
+ ## `relay_send(node, subject, body, timeout=None) -> dict`
72
+
73
+ **The same call behaves differently for the two kinds.** This is the part that
74
+ gets missed.
75
+
76
+ A **message** returns at once:
77
+
78
+ ```python
79
+ {"status": "delivered", "node": "fix-auth/session3", "id": "…"}
80
+ {"status": "sent", "node": "fix-auth/session3", "id": "…", "delivery": "…"}
81
+ ```
82
+
83
+ `delivered` is in their inbox now. `sent` waits for their machine, and
84
+ `delivery` says how it will get there.
85
+
86
+ On a **forced** relay it blocks for the child's whole turn and returns its
87
+ answer:
88
+
89
+ ```python
90
+ {"status": "done" | "timeout" | "error" | "gone" | "closed", "node": ..., "reply": str}
91
+ ```
92
+
93
+ **Check `status` before reading `reply`.** It is empty on `error`, where the
94
+ reason is in `error` instead — a crashed child otherwise reads exactly like one
95
+ that had nothing to say. `timeout` leaves the relay open and the child running,
96
+ so you can send again or close.
97
+
98
+ `subject` is one line, at most 120 characters: it rides in the tail of the other
99
+ agent's tool result, where a paragraph is noise. `timeout` applies to forced
100
+ sends only, defaults to 300 seconds — and, as with `session_join`, the exec's
101
+ own `timeout_seconds` must be higher or your code is killed mid-wait.
102
+
103
+ ## `relay_inbox(msg=None) -> dict`
104
+
105
+ Everything by default:
106
+
107
+ ```python
108
+ {"messages": [...], "count": int, "unread": int, "sent": [...], "forced": [...]}
109
+ ```
110
+
111
+ Each message is `{msg, from, reply_to, subject, body, at, sent_at}`. `from` is
112
+ the name to answer with. `sent` is what you have sent, each with `status`
113
+ `delivered` or `waiting`. `forced` lists any forced relay you are part of and
114
+ your role in it.
115
+
116
+ With `msg=n`, just that one. **A number that does not exist returns
117
+ `{"message": None, "available": [...]}`** rather than raising — the number came
118
+ out of a notice you may have half-remembered, and killing the exec over a typo
119
+ would destroy the work around it.
120
+
121
+ Message numbers are a message's position in your transcript, so they are never
122
+ reused and never renumbered.
123
+
124
+ ## `relay_connect(node, force=True, reason=None) -> dict`
125
+
126
+ Takes a forced relay on a direct child. Returns `{status: 'open' |
127
+ 'already-open', node, connection_id, forced}`. Without `force` it returns
128
+ `{status: 'not-needed'}` — just send.
129
+
130
+ **Raises** for an unresolvable node, and for forcing anything that is not a
131
+ direct child running here, a child its state machine is driving, or a second
132
+ child while one is open.
133
+
134
+ ## `relay_close(node) -> dict`
135
+
136
+ Ends a forced relay. Returns `{status: 'closed' | 'not-open', node}`. It does
137
+ **not** terminate anything — the child keeps running and keeps its work. Only the
138
+ parent may close one. A plain message has nothing to close.
139
+
140
+ ## `relay_accept` / `relay_decline`
141
+
142
+ Kept so older scripts run; both return `{status: 'not-needed'}`. There is no
143
+ request to answer any more. To say no, reply saying so, or do not reply.
144
+
145
+ ## How a message reaches you
146
+
147
+ You are told **once**, in the tail of a tool result, that mail arrived:
148
+
149
+ ```
150
+ ──
151
+ Relay: 1 new message is in your inbox.
152
+ msg 7 from fix-auth/session3 — "schema for the auth tables"
153
+ Read them with `await api.relay_inbox()`, or one with `api.relay_inbox(msg=N)`.
154
+ ```
155
+
156
+ That notice is not repeated and nothing is waiting on you. Finish what you are
157
+ doing and read it at a natural break.
158
+
159
+ If you were idle, the person running you is asked whether to wake you for it —
160
+ or, in Autonomous mode, you are woken without asking, unless the message came
161
+ from somebody else's account.
162
+
163
+ ## Examples
164
+
165
+ Messaging a peer:
166
+
167
+ ```python
168
+ await api.relay_send("fix-auth/session3", "schema", "Which columns does users gain?")
169
+ # ... carry on working; the answer arrives in your inbox ...
170
+ return await api.relay_inbox()
171
+ ```
172
+
173
+ Orchestrating a child:
174
+
175
+ ```python
176
+ await api.relay_connect("fix-auth/session4", force=True)
177
+ r = await api.relay_send("fix-auth/session4", "scope", "Do the reads first, leave writes to me.",
178
+ timeout=600)
179
+ if r["status"] != "done":
180
+ return {"child": r["status"], "why": r.get("error")}
181
+ verdict = r["reply"]
182
+ await api.relay_close("fix-auth/session4")
183
+ return verdict
184
+ ```
185
+
186
+ ## Getting it wrong
187
+
188
+ **Reading `reply` without checking `status`.** The `session_join` failure,
189
+ exactly: a child that crashed comes back with an empty `reply` and looks like a
190
+ child that succeeded quietly.
191
+
192
+ **Sending again because it says `waiting`.** It is in your transcript and will
193
+ be delivered when their machine reads it. A second send is a second message.
194
+
195
+ **Leaving the exec timeout at its default on a forced send.** `timeout=600`
196
+ inside a tool call capped at 120 seconds means your code dies at 120 and the
197
+ child is left mid-turn.
198
+
199
+ **Forcing a grandchild, or a session you did not spawn.** It raises. Forcing
200
+ goes around whoever is actually orchestrating that session, which is the one
201
+ thing it must never do.
202
+
203
+ **Treating `relay_close` as terminate.** It ends the forced relay. The child
204
+ carries on. `session_terminate` is the other thing, and it is not this.
205
+
206
+ **Treating the tail notice as an interrupt.** It is announced once and then
207
+ waits. Abandoning a half-finished edit to read it is worse than reading it two
208
+ minutes later.
209
+
210
+ **Expecting a subagent to have an inbox.** `agent` runs a subagent, not a
211
+ session — it has no transcript of its own, so it cannot be an endpoint.
212
+
213
+ ## See also
214
+
215
+ `session_join`, `agent`, `session_fork`, `sessions`, `read_bulletin`,
216
+ `bulletin`, `agent-vs-fork`
@@ -0,0 +1,72 @@
1
+ <!-- also: answer -->
2
+ <!-- group: Talking to the user -->
3
+
4
+ # api.reply(text) -> dict
5
+
6
+ Answer a question without taking the step away.
7
+
8
+ For what comes back as `asked`. The instruction, the ring and the step counter
9
+ all stay exactly as they were, and the answer appears on its own line
10
+ underneath — a word with somebody about the step they are on, rather than a new
11
+ step.
12
+
13
+ ## Do not answer with `instruct`
14
+
15
+ `instruct` **is** a new step. It moves the ring, resets the counter and replaces
16
+ the words — so answering a question with it takes away the very sentence the
17
+ question was about, which is the thing somebody was asking you to explain.
18
+
19
+ That was the original behaviour and it was reported as exactly what it looked
20
+ like: asking cost you your place. A question is a side conversation about the
21
+ step, not a reason to leave it.
22
+
23
+ ## Options
24
+
25
+ - **`text`** — the answer, in a sentence or two. It sits on a live card beside
26
+ the app, so it is skimmed rather than read; if the real answer is long, say
27
+ the short version here and the rest in the chat.
28
+
29
+ ## Returns
30
+
31
+ `{"status": "answered"}`, or `{"error": ...}` when there is no card up to answer
32
+ on — which means the tour already ended, and the reply has nowhere to go.
33
+
34
+ ## Examples
35
+
36
+ The loop, with the answer folded in:
37
+
38
+ ```python
39
+ while True:
40
+ r = await api.await_user(target)
41
+ if r.get("clicked") or r.get("advanced"):
42
+ break
43
+ if q := r.get("asked"):
44
+ await api.reply(answer_for(q))
45
+ continue # same step, still ringed, handed back to them
46
+ if r.get("stopped"):
47
+ break
48
+ ```
49
+
50
+ A question that deserves to move the tour instead:
51
+
52
+ ```python
53
+ if q := r.get("asked"):
54
+ await api.reply("That one installs the CLI — let me show you.")
55
+ target = api.find(role="button", text_exact="Install")
56
+ await api.instruct("Here it is.", target, title="detour")
57
+ ```
58
+
59
+ ## Getting it wrong
60
+
61
+ **Replying when nothing asked.** There is no card to write on once a tour has
62
+ ended, and the error says so rather than drawing one from nowhere.
63
+
64
+ **Answering in the chat and carrying on.** The tour is in its own window. An
65
+ answer typed into this conversation is in a window they are not looking at.
66
+
67
+ **Treating a question as an interruption.** It is the tour working. Answer it,
68
+ and go wherever it points — you are a guide, not a script.
69
+
70
+ ## See also
71
+
72
+ `await_user`, `instruct`, `highlight`
@@ -0,0 +1,78 @@
1
+ <!-- also: reply, answer-back, inbox-reply -->
2
+ <!-- group: Talking to the user -->
3
+
4
+ # api.reply_message(id, text) -> dict
5
+
6
+ Answer back, in the inbox row you are being asked about.
7
+
8
+ When somebody answers one of your requests you are woken with what they said.
9
+ Often what they said is a question — and until this existed there was nowhere to
10
+ put the answer, so it went into the transcript.
11
+
12
+ **That is the same as not answering.** They asked in the row; they are watching
13
+ the row; your transcript is a different surface they are not looking at. This is
14
+ the failure that produced this call: an agent asked about a file, the reply was
15
+ *"what's the context of this?"*, and the explanation went somewhere the person
16
+ who asked never saw.
17
+
18
+ ## The row comes back to them, to read
19
+
20
+ Replying puts the message back to unread — the dot comes back and the reply box
21
+ is theirs again — but it does **not** put it back to waiting on them. They
22
+ answered it once; that settled it. The task is not held in Blocked by your
23
+ reply, and it is not counted on the badge.
24
+
25
+ So if you still need something from them before you can go on — a verdict, a
26
+ decision, an action — file it as its own request after replying
27
+ (`request_verification`, `request_answer`, `request_operator_action`). That one
28
+ waits until it is answered, and it is drawn on the task's Card tab where they
29
+ can answer it.
30
+
31
+ That is also the reason not to reply with nothing. Every reply costs them a
32
+ second look at a row they had finished with.
33
+
34
+ ## Options
35
+
36
+ - **`id`** — the message you are answering. It came back from the request you
37
+ sent, and the prompt that woke you names it. You may only reply to your own
38
+ messages; anything else is refused.
39
+ - **`text`** — what you want to say. Plain text; blank lines are paragraphs.
40
+
41
+ ## Say it the way they asked it
42
+
43
+ The register rule from `api.man('request_answer')` applies here and matters
44
+ more, because they have already told you once that they could not follow you.
45
+
46
+ Answer the question they actually asked. If they asked what something is about,
47
+ that is a question about *their* work and their reasons — not about your call
48
+ stack. Do not explain the mechanism unless the mechanism is what they asked
49
+ about.
50
+
51
+ ```python
52
+ # they asked: "what's the context of this?"
53
+
54
+ # no
55
+ await api.reply_message(id, "It's the CSV parser refactor in legacy/csv.ts — "
56
+ "the shared implementation lacks the quoted-newline "
57
+ "handling that the local one has.")
58
+
59
+ # yes
60
+ await api.reply_message(id, "Sorry — it came out of the import work you asked "
61
+ "for last week. Two of the files read data in an "
62
+ "older way than the rest, and I wanted to know "
63
+ "whether to bring them in line now or leave them.")
64
+ ```
65
+
66
+ ## It does not block
67
+
68
+ Your turn finishes here. Their next answer wakes you again, the same way the
69
+ first one did.
70
+
71
+ ## Returns
72
+
73
+ `{'status': 'queued', 'id': ...}` — a receipt, not an answer.
74
+
75
+ ## See also
76
+
77
+ - `api.man('request_answer')` — asking in the first place, and the register rule
78
+ - `api.man('request_user_action')` — handing over work rather than a question
@@ -0,0 +1,118 @@
1
+ # api.report_bug(attempted, expected, actual, severity, kind='defect', surface=None, blocked=False, workaround=None, reproduction=None, citations=None) -> dict
2
+ <!-- group: Reporting a WidgeX problem -->
3
+
4
+ File a bug against **WidgeX** — the environment you are running in. Not gated.
5
+
6
+ This has nothing to do with the project you are editing. You are almost
7
+ certainly working on someone's codebase; a limitation you hit *while* doing that
8
+ is exactly the report that is wanted.
9
+
10
+ File when an api call misbehaves, a capability does not exist, the sandbox
11
+ refuses something legitimate, a permission prompt hangs, a browser will not
12
+ attach, or a fork or harness fails.
13
+
14
+ ## Options
15
+
16
+ - **`attempted`** — what you were trying to accomplish, in your own terms.
17
+ - **`expected`** — what you expected the environment to let you do.
18
+ - **`actual`** — what happened instead.
19
+ - **`severity`** — impact **on your ability to work**, not code severity:
20
+ `'critical'` (blocked, no workaround), `'major'`, `'minor'`, `'cosmetic'`.
21
+ - **`kind`** — `'defect'` when something misbehaved, `'missing_capability'`
22
+ when there was simply no way to do it, `'unclear'` when you cannot tell.
23
+ Default `'defect'`. **`'unclear'` is a real answer** and better than a
24
+ confident guess.
25
+ - **`surface`** — where you hit it: `files`, `shell`, `search`, `sessions`,
26
+ `harness`, `browser`, `xylon`, `permissions`, `ui`, `other`.
27
+ - **`blocked`** — `True` only if it stopped the work. If you got there another
28
+ way, this is `False` and the way is `workaround`.
29
+ - **`workaround`** — what you did instead.
30
+ - **`reproduction`** — minimal steps, if you know them.
31
+ - **`citations`** — WidgeX source lines, e.g. `["StdlibShell.ts:301"]`. Normally
32
+ omitted; you are not expected to know its internals.
33
+
34
+ **Write prose in every field.** Session state, environment, a transcript
35
+ excerpt, this turn's diff and recent logs are attached automatically. Pasting a
36
+ stack trace in `actual` displaces the sentence that would have been useful.
37
+
38
+ ## Returns
39
+
40
+ ```python
41
+ {"report_id": str, "path": str, "similar": [...]}
42
+ ```
43
+
44
+ `similar` lists earlier reports with the same signature. **Check it before
45
+ filing the same thing twice.**
46
+
47
+ ## Examples
48
+
49
+ A call that did not do what its signature says:
50
+
51
+ ```python
52
+ return await api.report_bug(
53
+ attempted="load the github widget by name so I could drive the PR page",
54
+ expected="browser_load(name='github') to register that widget's states",
55
+ actual="it was accepted but the registry stayed empty, and browser_context "
56
+ "reported no widget loaded",
57
+ severity="major",
58
+ kind="defect",
59
+ surface="browser",
60
+ blocked=False,
61
+ workaround="passed the state definitions as code= instead",
62
+ )
63
+ ```
64
+
65
+ Something that simply does not exist:
66
+
67
+ ```python
68
+ return await api.report_bug(
69
+ attempted="unbind the state machine from this harness while re-arming it",
70
+ expected="some way to clear the binding from the api",
71
+ actual="omitting machine= keeps the existing binding, and there is no value "
72
+ "that clears it — the only route is the Harness Manager UI",
73
+ severity="minor",
74
+ kind="missing_capability",
75
+ surface="harness",
76
+ )
77
+ ```
78
+
79
+ Not knowing what went wrong is fine:
80
+
81
+ ```python
82
+ return await api.report_bug(
83
+ attempted="join a child session I had just extended",
84
+ expected="status done or timeout",
85
+ actual="status error with an empty reason; the child's node looks complete",
86
+ severity="major",
87
+ kind="unclear",
88
+ surface="sessions",
89
+ blocked=True,
90
+ )
91
+ ```
92
+
93
+ ## Getting it wrong
94
+
95
+ **Not filing because it is probably your fault.** Partial ownership is enough.
96
+ "I may have called it wrong" and "the environment is plausibly involved" are
97
+ both true at once, and only one of them is a reason to file.
98
+
99
+ **Not filing because you are not working on WidgeX.** You are never working on
100
+ WidgeX. That is the normal case.
101
+
102
+ **Treating "there is no way to do X" as a dead end.** It is a report —
103
+ `kind='missing_capability'` — and then you find another route. File and keep
104
+ going.
105
+
106
+ **Pasting logs, diffs or stack traces.** They are attached already, and the
107
+ fields cap out. Describe what happened.
108
+
109
+ **Filing once per retry.** One report per distinct limitation. Check `similar`.
110
+
111
+ **Telling the user you are blocked when you are not.** If you worked around it,
112
+ `blocked=False` and put the workaround in the report — not in an apology.
113
+
114
+ **Using it for a bug in the user's code.** That is not this. This is the IDE.
115
+
116
+ ## See also
117
+
118
+ `sandbox`, `permissions`, `overview`
@@ -0,0 +1,142 @@
1
+ <!-- also: ask, question, decide, decision, which one -->
2
+ <!-- group: Talking to the user -->
3
+
4
+ # api.request_answer(question, detail=None, choices=None) -> dict
5
+
6
+ Ask the user to decide something you cannot decide yourself. **Does not block.
7
+ Not gated.**
8
+
9
+ The other three handoffs give away *work* — a GUI task, a judgement.
10
+ `request_answer` gives away a **decision**: two defensible options and no way
11
+ for you to pick between them from inside the code.
12
+
13
+ ## It does not block
14
+
15
+ Your turn finishes. The question waits as a row in the user's inbox, and
16
+ nothing expires. When they answer, you are prompted with what they said and
17
+ carry on from there.
18
+
19
+ So the call is the **end** of a turn, not a pause in the middle of one. Say what
20
+ you are waiting on and stop:
21
+
22
+ ```python
23
+ await api.request_answer(
24
+ "Standardise how we read CSV files, or leave the odd one out?",
25
+ detail="Standardising means one way to maintain instead of two, and a day's work "
26
+ "now. Leaving it means the old path stays until something breaks it. "
27
+ "Either is fine by me — it is your call which we live with.",
28
+ choices=["standardise", "leave it"],
29
+ )
30
+ return "Asked about the CSV readers — everything else is done."
31
+ ```
32
+
33
+ Writing the rest of the turn as though the answer were in hand is the way this
34
+ goes wrong. There is no answer in the return value and there will not be one
35
+ before your turn ends.
36
+
37
+ ## Who is reading it
38
+
39
+ Somebody who has not seen the code, and will not go and look.
40
+
41
+ This is the thing agents get wrong first, and it is not a style preference — it
42
+ decides whether the question is answerable at all. Your message lands in an
43
+ inbox beside the other things a person has to decide today. They will not open
44
+ the file. They do not know what the class is called. They will not ask you a
45
+ follow-up before deciding; they will answer wrongly, or leave it.
46
+
47
+ So: no file paths, no symbol names, no branch names, no test names. Name the
48
+ **trade-off**, in the words you would use saying it out loud.
49
+
50
+ A real message, and the reply it actually got:
51
+
52
+ > **Convert legacy/csv.ts to the shared parser?**
53
+ >
54
+ > It has hand-rolled quote handling that gets embedded newlines inside a quoted
55
+ > field right, and the shared parser does not. Convert it and we lose that case,
56
+ > which the fixtures do exercise. Leave it and we keep two parsers.
57
+
58
+ > — *what's the context of this?*
59
+
60
+ The same decision, asked so it can be answered:
61
+
62
+ > **Two ways to handle CSV imports — which matters more here?**
63
+ >
64
+ > One copes with unusual files that the other mangles. The other is half the
65
+ > code to maintain. I can go either way; I only need to know which you would
66
+ > rather live with.
67
+
68
+ Nothing was removed but the vocabulary. The second is shorter, and it is the
69
+ one a person can act on without opening anything.
70
+
71
+ **Three lines is plenty.** If the decision will not go in three, the thing to
72
+ send is a smaller decision.
73
+
74
+ ## Options
75
+
76
+ - **`question`** — what you want decided, as a full sentence, in the user's
77
+ terms. It is the headline of a row read at a glance beside others, so it has
78
+ to make sense to somebody who has not read your transcript.
79
+ - **`detail`** — **what you will do with each answer.** The user is choosing
80
+ between outcomes, not between words. A question that does not say what
81
+ follows from it is one they cannot answer well, and they will ask you what
82
+ you meant instead of answering — a whole round trip for a sentence you could
83
+ have written.
84
+ - **`choices`** — labels that become buttons beside the reply box. A shortcut,
85
+ not a constraint: the reply box is always there and the user can write
86
+ something you did not offer. **Read what they wrote, do not match on the
87
+ label.**
88
+
89
+ ## Returns
90
+
91
+ ```python
92
+ {"status": "queued", "id": "inbox-1757280000000-a1b2c3"}
93
+ ```
94
+
95
+ A **receipt, not an answer**. `id` is the row it left behind. Nothing in this
96
+ dict tells you what the user decided, because at the moment it returns they have
97
+ not seen the question.
98
+
99
+ ## Getting it wrong
100
+
101
+ **Comparing the answer against your own `choices` with `==`.** When the answer
102
+ arrives it is prose the user typed, and a label they clicked is only the most
103
+ likely case.
104
+
105
+ ```python
106
+ # Wrong — silently takes the else branch when they typed anything at all.
107
+ if answer == "convert it":
108
+ ...
109
+
110
+ # Right — read it.
111
+ if "convert" in answer.lower():
112
+ ...
113
+ ```
114
+
115
+ **Treating the return value as the decision.** `{"status": "queued"}` means the
116
+ question was filed, not answered. Branching on it branches on nothing.
117
+
118
+ **Asking what you could find out.** If the answer is in the repo, read the repo.
119
+ A question costs the user attention you can only spend once, and a row they did
120
+ not need is a row that makes the next one slower to reach.
121
+
122
+ **Asking for reassurance about a plan.** Permission prompts already cover the
123
+ gated calls. This is for a decision that changes *what* you build.
124
+
125
+ **Sending news.** An inbox row is something to act on. "Rebased onto main"
126
+ belongs in your reply; there is no request for it, and there is deliberately no
127
+ way to file one.
128
+
129
+ ## See also
130
+
131
+ `ask_user` — the same question asked *now*, blocking your turn until they pick.
132
+ Reach for it when the answer changes what the very next line of code does and
133
+ you are sitting in a session somebody is watching. Reach for `request_answer`
134
+ when the answer changes what happens next *session*, or when nobody is watching
135
+ — eight agents finishing overnight cannot each hold a turn open on a human who
136
+ is asleep.
137
+
138
+ `request_review` — the same non-blocking shape for "this is ready to look at"
139
+ rather than "I need you to choose".
140
+
141
+ `request_user_action`, `request_operator_action`, `phase_complete`,
142
+ `man('overview')`