@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,99 @@
1
+ # api.run(argv, input=None, cwd=None, env=None, timeout=120, background=False) -> dict
2
+ <!-- group: Coding -->
3
+ <!-- also: subprocess, exec, argv, stdin, python -c, node -e, quoting, no shell -->
4
+
5
+ Run one program with exactly these arguments. **No shell. Gated** (it asks as
6
+ `bash`, showing the equivalent command line). **Never raises — read
7
+ `exit_code`.**
8
+
9
+ ## Why this exists
10
+
11
+ A command used to cross up to four interpreters on its way to the program:
12
+ your Python string, then `bash -c`, then often PowerShell or `python -c` or
13
+ `node -e` or a heredoc. Each layer has its own escapes, and none of them shows
14
+ you what the next one received. Python ate the backslash in `'\''`; bash ate
15
+ `$_` before PowerShell saw it; a regex lost its `\$` and silently matched
16
+ nothing. Those were the commonest ways scripts here broke.
17
+
18
+ `run` removes the layers instead of asking you to get them right. `argv` goes
19
+ to the operating system as a list, so `argv[1]` arrives as `argv[1]` — nothing
20
+ is expanded, split, globbed or quoted. And a script for another interpreter
21
+ goes in as **`input`**, on stdin, so it is never on a command line at all.
22
+
23
+ ## Examples
24
+
25
+ ```python
26
+ r = await api.run(["git", "log", "--format=%h %s", "-5"])
27
+ ```
28
+
29
+ A script in another language — the source is a value, not something to quote:
30
+
31
+ ```python
32
+ src = r'''
33
+ import sys, re
34
+ print(re.findall(r"\$[0-9.]+", sys.stdin.read()))
35
+ '''
36
+ r = await api.run(["python", "-"], input=src)
37
+ r = await api.run(["node", "-"], input="console.log(process.version)")
38
+ ```
39
+
40
+ Arguments with spaces, quotes and dollar signs need nothing:
41
+
42
+ ```python
43
+ await api.run(["git", "commit", "-m", "Fix the 'it's' case; costs $0"])
44
+ ```
45
+
46
+ Elsewhere, with extra environment:
47
+
48
+ ```python
49
+ await api.run(["pnpm", "test"], cwd="widgex-ide/extension", env={"CI": "1"}, timeout=900)
50
+ ```
51
+
52
+ ## Options
53
+
54
+ - **`argv`** — a list of strings. A single string is refused: that is a command
55
+ line, and `api.bash` is the call for those.
56
+ - **`input`** — `str` or `bytes`, written to stdin, which is then closed. With
57
+ no `input`, stdin is empty.
58
+ - **`cwd`** — any path `api.*` accepts, including `/c/...` and `~`.
59
+ - **`env`** — set on top of the inherited environment, never instead of it.
60
+ - **`timeout`**, **`background`** — as for `api.bash`; a background run is
61
+ managed with `bash_output`, `bash_list` and `bash_kill`.
62
+
63
+ `argv[0]` is looked up on `PATH` the way a shell would, and the result carries
64
+ the full path in `program`, so "which python was that" has an answer. On
65
+ Windows the Microsoft Store's `python.exe` stubs are skipped. A path with a
66
+ directory in it — `./scripts/x.mjs` — is relative to `cwd`, or the shell's cwd.
67
+
68
+ **Batch files on Windows.** `npm`, `pnpm` and `npx` are `.cmd` files, and
69
+ Windows runs those through cmd.exe, which reads `& | < > ^ % "` in their
70
+ arguments as cmd syntax. There is no escaping that is safe for every batch
71
+ file, so `run` refuses such an argument (exit code 2, with the reason) rather
72
+ than pass it on. Plain arguments to `.cmd` files work as normal.
73
+
74
+ The permission card shows the call as the bash command it amounts to:
75
+ `env` as `VAR=value` before it, and `input` as a heredoc after it, so what
76
+ you approve is everything the call does.
77
+
78
+ ## Returns
79
+
80
+ ```python
81
+ {"stdout": str, "stderr": str, "exit_code": int, "program": str}
82
+ ```
83
+
84
+ `exit_code` 127 with `"…: not found on PATH"` means the program does not exist.
85
+ `-1` means it timed out or could not start, exactly as for `bash`.
86
+
87
+ ## Getting it wrong
88
+
89
+ **Putting a pipeline in `argv`.** `["ls", "|", "grep", "x"]` passes `|` to
90
+ `ls`. Pipes, globs and `&&` are shell features: use `api.bash` for those, or do
91
+ the filtering in Python.
92
+
93
+ **Writing the script as a normal string.** `input="print('a\nb')"` is Python
94
+ source in *your* Python first, and `\n` there is a newline before `run` ever
95
+ sees it. Use a raw string, `r'''…'''`, for any source with backslashes.
96
+
97
+ ## See also
98
+
99
+ `bash`, `powershell`, `sandbox`
@@ -0,0 +1,144 @@
1
+ # The sandbox
2
+ <!-- group: Concepts -->
3
+
4
+ Why `import`, `open()` and `os.*` are refused, and what to use instead.
5
+
6
+ Your code runs in a real Python process with a PEP 578 audit hook installed.
7
+ The hook sees every security-sensitive operation at the C level and, once
8
+ installed, cannot be removed by anything you execute. It is not a linter and
9
+ not a name filter — rebinding, `getattr`, or reaching through a builtin all hit
10
+ the same wall.
11
+
12
+ ## The rule
13
+
14
+ Inside the code you write, **only `api` reaches the outside world.**
15
+
16
+ Refused in your code:
17
+
18
+ - `open()`, and every other direct file access
19
+ - `os.system`, `os.popen`, `subprocess.*`, `socket`, and importing them
20
+ - `eval`, `exec`, `compile`, `input`, `breakpoint`
21
+ - `ctypes` and friends, which would let native code step around the hook
22
+ - `time.sleep`, which would freeze the whole shell — `await asyncio.sleep(n)`
23
+
24
+ Allowed in your code: ordinary Python, **including imports of the pure
25
+ standard library**. These import normally:
26
+
27
+ ```
28
+ abc ast base64 binascii bisect calendar cmath collections collections.abc
29
+ contextlib copy csv dataclasses datetime decimal difflib enum fnmatch
30
+ fractions functools gzip hashlib heapq hmac html html.parser inspect io
31
+ itertools json keyword math numbers operator pprint random re secrets shlex
32
+ statistics string struct textwrap tomllib traceback types typing unicodedata
33
+ urllib urllib.parse uuid xml.etree.ElementTree zlib
34
+ asyncio os os.path pathlib time
35
+ ```
36
+
37
+ `os` is the sandboxed one (paths, no environment) and `time` has no `sleep`;
38
+ `json`, `os`, `Path` and `asyncio` are also bound without importing. None of
39
+ these can reach the system on their own: a file, process or socket any of them
40
+ tries to open is refused by the same hook as your own `open()`. A refused
41
+ import raises `ImportError`, so `try: … except ImportError:` works.
42
+
43
+ The same operations *are* permitted inside an `api.*` method's own body. That
44
+ is not an inconsistency: an api call is a declared, named, journalled,
45
+ possibly-gated action, and the whole point of routing through `api` is that the
46
+ IDE can see it. Your code calling `open()` directly is a file access nobody
47
+ recorded.
48
+
49
+ ## What to use instead
50
+
51
+ | You reached for | Call this |
52
+ |---|---|
53
+ | `open(p).read()` | `await api.read_file(p, raw=True)` — the exact text, no line numbers |
54
+ | `open(p, "w").write(s)` | `await api.write_file(p, s)` — `s` may be `bytes` |
55
+ | `os.listdir(d)` | `await api.ls(d)` |
56
+ | `os.path.exists(p)` | `await api.glob(p)` — empty list means no |
57
+ | `glob.glob(pat)` | `await api.glob(pat)` |
58
+ | `subprocess.run(argv)` | `await api.run(argv)` — no shell, nothing to quote |
59
+ | a shell pipeline | `await api.bash(cmd)` |
60
+ | PowerShell | `await api.powershell(script)` |
61
+ | a third-party module | `await api.run(["python", "-"], input=script)` |
62
+ | `os.chdir(d)` | `await api.cd(d)` |
63
+ | `requests.get(url)` | `await api.run(["curl", "-s", url])`, or the browser calls |
64
+
65
+ **Do not reach for `api.bash("python -c ...")` to get at a module.** It is a
66
+ second interpreter, outside this sandbox, behind two more layers of quoting —
67
+ the commonest way scripts here broke. If the module is in the list above,
68
+ import it; if it is not, pass the script as `input=` to `api.run`, where
69
+ nothing is quoted at all.
70
+
71
+ `api.bash`, `api.run` and `api.powershell` are the escape hatch. What they run
72
+ is a normal process — it is not sandboxed, because the *user approved that
73
+ specific command*. That approval is what the sandbox is protecting.
74
+
75
+ ## Modes
76
+
77
+ The shell runs in one of several sandbox modes chosen by the IDE. `strict` is
78
+ the tightest and is what the rules above describe. Looser modes widen which
79
+ paths an api method may touch; none of them let your own code import or open
80
+ anything. Write code that works under `strict` and it works everywhere.
81
+
82
+ ## Seals
83
+
84
+ An organisation can **seal** a file or a folder against some or all models —
85
+ read, write, execute, copy or move. A seal is not a permission: no mode, grant
86
+ or approval lifts it, and there is nobody to ask. When a call is refused with
87
+ "WidgeX Seal", carry on without that file and say in your reply that it was
88
+ sealed.
89
+
90
+ Every way of reaching a sealed file is refused, not only `read_file`:
91
+ `edit_file` (an edit reads), `grep` (the file is skipped and named), `bash`,
92
+ `run` and `powershell` commands that name it — directly, through a glob, a
93
+ `cd`, git's `rev:path`, or inline code that mentions it — `drive` or
94
+ `navigate` to it as a `file://` page, `aux_context=` and any other api that
95
+ would hand it to a model. A command that copies or moves a file out of a
96
+ sealed folder is refused too, because the copy would not be sealed.
97
+
98
+ Output is screened as well: a line of a read-sealed file that reaches your
99
+ result by any route is replaced with a notice saying it was withheld. Do not try
100
+ to recover withheld lines another way — encoding or transforming a sealed file
101
+ to get it past the screen is exactly what a seal forbids, even where nothing
102
+ can detect it.
103
+
104
+ ## Examples
105
+
106
+ ```python
107
+ # Wrong — open() is refused by the audit hook, and the exec dies here.
108
+ import json
109
+ data = json.load(open("package.json"))
110
+
111
+ # Right.
112
+ import json
113
+ data = json.loads(await api.read_file("package.json", raw=True))
114
+ ```
115
+
116
+ Checking whether something exists:
117
+
118
+ ```python
119
+ hits = await api.glob("src/**/config.ts")
120
+ if not hits:
121
+ return "no config found"
122
+ return await api.read_file(hits[0])
123
+ ```
124
+
125
+ ## Getting it wrong
126
+
127
+ **Treating a refusal as a bug in your code and retrying with a different
128
+ spelling.** `__builtins__["open"]`, `os = __import__("os")` and every other
129
+ route hit the same hook. If an api call cannot do what you need, that is a
130
+ missing capability — say so with `api.report_bug(kind="missing_capability")`
131
+ and route around it, usually via `api.run`.
132
+
133
+ **Putting a whole file's contents into `api.bash` as a heredoc to avoid
134
+ `write_file`.** It works, but `bash` changes are not journalled: they will not
135
+ show up in `xylon_get_diff` and cannot be reverted with the turn. Use
136
+ `write_file` and `edit_file` for file contents.
137
+
138
+ **Assuming a refusal is silent.** It raises, which aborts the entire exec —
139
+ every line after it in that block never runs. Keep sandbox-risky experiments in
140
+ their own small exec.
141
+
142
+ ## See also
143
+
144
+ `permissions`, `bash`, `read_file`, `write_file`, `report_bug`
@@ -0,0 +1,74 @@
1
+ # api.session_fork(prompt, aux_context=None) -> dict
2
+ <!-- group: Sessions -->
3
+
4
+ Fork into an independent clone: copied repo, copied conversation. Gated.
5
+
6
+ The child runs in a **cloned copy of the repository** and starts with a copy of
7
+ this conversation's history. Your `prompt` becomes its opening task. It runs
8
+ independently; this call returns as soon as it exists.
9
+
10
+ ## Options
11
+
12
+ - **`prompt`** — the forked session's starting task. Unlike `agent`,
13
+ the child already knows what you have been doing, so this can be short: it is
14
+ the instruction, not the briefing.
15
+ - **`aux_context`** — path to a `.md` or `.txt` file, absolute or relative to
16
+ the cwd. Its **contents** are read now and attached to the child's opening
17
+ prompt. Capped at 100,000 characters. Read on this side because the fork runs
18
+ in a *different* checkout — a path you passed might not exist there.
19
+
20
+ There is no `model` and no `summary`. The conversation copy is the briefing.
21
+
22
+ ## Returns
23
+
24
+ ```python
25
+ {"sessionId": str, "xylonNodePath": str}
26
+ ```
27
+
28
+ Immediately. The fork exists; it has not finished.
29
+
30
+ ## Examples
31
+
32
+ Trying an approach without committing the user's tree to it:
33
+
34
+ ```python
35
+ child = await api.session_fork(
36
+ "Try the approach we just rejected — swap the audit hook for a subprocess "
37
+ "jail — and report whether the test suite passes. Change nothing else."
38
+ )
39
+ return f"Forked to {child['sessionId']}; its edits are in a clone, not here."
40
+ ```
41
+
42
+ Two approaches in parallel, compared afterwards:
43
+
44
+ ```python
45
+ a = await api.session_fork("Implement it with a ContextVar, as discussed.")
46
+ b = await api.session_fork("Implement it by threading the id through explicitly.")
47
+ ra = await api.session_join(a["sessionId"], timeout=1800)
48
+ rb = await api.session_join(b["sessionId"], timeout=1800)
49
+ return {"contextvar": ra.get("lastMessage"), "explicit": rb.get("lastMessage")}
50
+ ```
51
+
52
+ ## Getting it wrong
53
+
54
+ **Expecting the fork's edits to appear in your repo.** They will not. The clone
55
+ is a separate checkout, and nothing merges it back. A fork's output is what it
56
+ *tells* you, plus a diff you can read from its xylon node. If you want the
57
+ files changed here, do not fork.
58
+
59
+ **Forking for routine parallelism.** Cloning a repository is expensive, and two
60
+ forks of a large tree cost real disk and real time. `agent` shares the
61
+ directory; `agent` shares even more.
62
+
63
+ **Forking to give a child context.** It works — the history is copied — but you
64
+ copy *everything*, including whatever is irrelevant. `agent(wait=False,
65
+ summary=...)` gives the child three sentences instead of an hour.
66
+
67
+ **Forgetting it is gated and long-lived.** The user approves a fork; it then
68
+ sits there consuming tokens until it finishes or is terminated. You can
69
+ `session_terminate` a fork, because it is your descendant.
70
+
71
+ ## See also
72
+
73
+ `agent`, `session_join`, `session_terminate`,
74
+ `agent-vs-fork`, `sessions`, `xylon`
@@ -0,0 +1,109 @@
1
+ # api.session_harness(prompt, token_budget=None, window='day', machine=None, model=None) -> dict
2
+ <!-- group: Sessions -->
3
+ <!-- also: session_daemonize -->
4
+
5
+ Turn this session into a harness: the same prompt, re-served every turn. Gated.
6
+
7
+ A harness runs unattended. At the start of each turn it is handed `prompt` again
8
+ and works until the turn ends; then it starts over. It stops when
9
+ `api.session_rest` is called, when its machine reaches an end state, when a
10
+ human stops it, or when it reaches a limit:
11
+
12
+ - **tokens** — `token_budget` per window, 1,000,000 per day if you give none.
13
+ There is no "no limit".
14
+ - **cycles** — 50 in one run.
15
+
16
+ Reaching a limit stops the harness until a person presses Run in the Harness
17
+ Manager. It does not wait and resume by itself.
18
+
19
+ **A harness cannot ask the user anything.** That constraint shapes everything
20
+ below.
21
+
22
+ ## Options
23
+
24
+ - **`prompt`** — the standing task, re-served verbatim every turn. Write it for
25
+ a session with no memory of the last run: what to look at, what counts as
26
+ work, and what to do when there is nothing to do.
27
+ - **`token_budget`** — tokens per window, 1,000 to 100,000,000. Omit it for
28
+ 1,000,000. Spending it stops the harness until a person restarts it.
29
+ - **`window`** — `'hour'`, `'day'` or `'week'`. Default `'day'`.
30
+ - **`machine`** — the name of a state machine in `.widgex/harnesses/`, which
31
+ splits the standing task into states with their own prompts and gated
32
+ transitions. Every state must have a path to a `"terminal": true` state, or
33
+ the machine does not load. See `harness` for the format.
34
+ - **`model`** — the model every run uses, e.g. `'claude-opus-5'`, or
35
+ `'provider/model'` when two providers share an id. An unknown id is refused,
36
+ with the list of valid ones, and **nothing is armed**.
37
+
38
+ **Omitting `machine` or `model` keeps whatever is currently set.** This is
39
+ deliberate: re-arming to change a budget must not silently unbind a machine.
40
+ There is no api route to *clear* a binding — that is done from the Harness
41
+ Manager.
42
+
43
+ ## Returns
44
+
45
+ ```python
46
+ {"sessionId": str, "runs": int, "spent": int,
47
+ "budget": {"tokens": int, "window": str},
48
+ "machine": {"name": str, "state": str} | None,
49
+ "model": {"providerId": str, "modelId": str} | None}
50
+ ```
51
+
52
+ `runs` and `spent` are the counters for the current window, so a re-arm shows
53
+ you what the previous arming had used.
54
+
55
+ ## Examples
56
+
57
+ A standing task with an explicit idle branch:
58
+
59
+ ```python
60
+ return await api.session_harness(
61
+ "Check the CI dashboard for newly failing tests on main. For each one, find "
62
+ "the commit that broke it and open an issue naming that commit. If nothing "
63
+ "is failing, say so in one line and stop for this turn.",
64
+ token_budget=200_000,
65
+ window="day",
66
+ )
67
+ ```
68
+
69
+ Raising a budget without disturbing the machine binding:
70
+
71
+ ```python
72
+ return await api.session_harness(prompt, token_budget=500_000, window="week")
73
+ ```
74
+
75
+ Stopping the loop from inside it:
76
+
77
+ ```python
78
+ if not failures:
79
+ await api.session_rest("main has been green for three consecutive runs")
80
+ return "Nothing failing — rested."
81
+ ```
82
+
83
+ ## Getting it wrong
84
+
85
+ **Writing a prompt that assumes the previous turn.** "Continue where you left
86
+ off" reaches a session that does not know where that was. State is in the repo
87
+ and the tree, not in the harness. Say how to find it.
88
+
89
+ **Calling `ask_user`.** It is denied outright — "this harness runs unattended and
90
+ cannot ask questions". Decide, or `session_rest` with the question as your
91
+ reason.
92
+
93
+ **Expecting a gated call to prompt.** It will not. Gated calls resolve from
94
+ grants: auto-allowed or auto-denied. A harness that needs `edit_file` and has no
95
+ grant for it will be refused every turn, forever.
96
+
97
+ **No exit condition.** Without a `session_rest` or an end state, the harness
98
+ runs until it hits its token or cycle limit and stops there, having finished
99
+ nothing. Decide what "finished" means before arming.
100
+
101
+ **Using `session_terminate` to stop it.** That destroys the session. `session_rest`
102
+ stops the loop and leaves it re-armable.
103
+
104
+ **Assuming `machine=None` unbinds.** It keeps the current machine. Unbinding is
105
+ a Harness Manager action.
106
+
107
+ ## See also
108
+
109
+ `harness`, `session_rest`, `permissions`, `ask_user`, `sessions`
@@ -0,0 +1,102 @@
1
+ # api.session_join(session_id, timeout=None) -> dict
2
+ <!-- group: Sessions -->
3
+
4
+ Wait for a child session to finish its current turn. Not gated.
5
+
6
+ Blocks until the target reaches `done` or `error`, or until the timeout — a
7
+ child still working then comes back as `timeout` and **keeps running**.
8
+
9
+ ## Options
10
+
11
+ - **`session_id`** — the id from `session_fork` or `agent`.
12
+ - **`timeout`** — seconds. Must satisfy `0 < timeout <= 3600`. Defaults to
13
+ **300**. A non-number, zero, or anything over an hour raises `ValueError`
14
+ before it waits.
15
+
16
+ **Raise the exec's own `timeout_seconds` above this**, or the tool call running
17
+ your code is killed while the join is still waiting. The tool call defaults to
18
+ 120 seconds, which is shorter than this call's own default of 300.
19
+
20
+ ## Returns
21
+
22
+ ```python
23
+ {"lastMessage": str, "xylonNodePath": str, "status": "done" | "error" | "timeout"}
24
+ ```
25
+
26
+ plus an **`error`** key when the run failed.
27
+
28
+ `status` is the only reliable signal:
29
+
30
+ | `status` | Meaning | `lastMessage` |
31
+ |---|---|---|
32
+ | `done` | The turn finished. | the child's answer |
33
+ | `error` | The run failed. | **empty** — read `error` |
34
+ | `timeout` | Still working. | whatever it had said so far |
35
+
36
+ A failed run has an **empty `lastMessage`**, because the reason is recorded as
37
+ a system message rather than an assistant one. An empty `lastMessage` on its
38
+ own cannot be told apart from a child that simply had nothing to say.
39
+
40
+ ## Examples
41
+
42
+ Check `status` first. Always:
43
+
44
+ ```python
45
+ r = await api.session_join(child_id, timeout=900)
46
+ if r["status"] == "error":
47
+ return {"child_failed": child_id, "why": r.get("error")}
48
+ if r["status"] == "timeout":
49
+ return f"{child_id} is still working; I'll leave it running."
50
+ return r["lastMessage"]
51
+ ```
52
+
53
+ Polling, rather than one long block:
54
+
55
+ ```python
56
+ for _ in range(4):
57
+ r = await api.session_join(child_id, timeout=600)
58
+ if r["status"] != "timeout":
59
+ break
60
+ await api.todo_write(todos) # keep the user informed between waits
61
+ return r
62
+ ```
63
+
64
+ Waiting on several children:
65
+
66
+ ```python
67
+ out = {}
68
+ for cid in child_ids:
69
+ r = await api.session_join(cid, timeout=300)
70
+ out[cid] = r["lastMessage"] if r["status"] == "done" else r["status"]
71
+ return out
72
+ ```
73
+
74
+ ## Getting it wrong
75
+
76
+ **Reading `lastMessage` without checking `status`.** This is the failure this
77
+ call is most known for:
78
+
79
+ ```python
80
+ # Wrong — a crashed child reports as a silent success.
81
+ r = await api.session_join(cid)
82
+ return r["lastMessage"] or "the child had nothing to add"
83
+ ```
84
+
85
+ You will tell the user the work is done when it failed.
86
+
87
+ **Leaving the exec timeout at its default.** `timeout=1800` inside a tool call
88
+ capped at 120 seconds means your code dies at 120 seconds and the child is
89
+ orphaned. Set the tool call's `timeout_seconds` higher than the join's.
90
+
91
+ **Treating `timeout` as a failure.** The child is still going. Either wait
92
+ again or report that it is running; do not restart the work.
93
+
94
+ **Joining a `agent`.** A delegate is not a session and has no id — it
95
+ already blocked and returned its answer directly.
96
+
97
+ **Joining something that is not your descendant.** Only sessions you spawned
98
+ are yours to wait on.
99
+
100
+ ## See also
101
+
102
+ `agent`, `session_fork`, `session_terminate`, `agent`, `sessions`
@@ -0,0 +1,73 @@
1
+ # Landing a child's branch (no api call — the user does this)
2
+
3
+ <!-- group: Sessions -->
4
+
5
+ Taking a branch child's work back into a parent branch. **There is no
6
+ `api.session_land`.** Landing is the "Land changes" wizard in the session
7
+ queue, and the user drives it.
8
+
9
+ This page is here because the operation is one you will be asked about and will
10
+ want to explain — and because it used to document a call, in enough detail that
11
+ an agent reading the manual would go on to invoke one that has never existed.
12
+ What follows is what landing does, so you can say so; the last section is what
13
+ to do instead of reaching for it.
14
+
15
+ ## Only what the child did
16
+
17
+ A child spawned with `api.agent(isolate=True)` gets a copy of the parent's
18
+ uncommitted work at the moment it was spawned. That copy is committed on the
19
+ child's branch so its own work can be measured against something, but it is
20
+ **not** the child's work and it does not come back — the parent already has it.
21
+
22
+ The consequence worth knowing when you are reading a landing preview: a child
23
+ that has done nothing of its own has **nothing to land**, even though its
24
+ branch carries a commit. It is reported as idle rather than merged. Merging it
25
+ would put the parent's own work back on top of the parent, and with three
26
+ children it would do so three times.
27
+
28
+ ## What the preview answers
29
+
30
+ The wizard previews before it merges, and the question it answers is the one
31
+ landing children one at a time cannot: **which files more than one child
32
+ touched**. Git will merge two children's edits to the same file perfectly
33
+ happily, and the result can still be one child silently undoing the other. The
34
+ overlap list names those paths and the branches that collide on them, while
35
+ there is still a choice about the order — or about whether to take both.
36
+
37
+ ## Outcomes you may be shown
38
+
39
+ - **`landed`** — its work is in the target branch.
40
+ - **`conflicted`** — git stopped. The pass stops with it.
41
+ - **`skipped`** — deliberately left out: mid-turn, blocked by uncommitted
42
+ work, or already in the target. The pass carries on.
43
+ - **`not-attempted`** — the pass had already stopped. Nothing was done to it.
44
+ - **`failed`** — it threw. Distinct from a conflict, which is a resting state.
45
+
46
+ ## If a landing conflicts, stop
47
+
48
+ Report what happened and stop. Do not go into the worktree and fix the merge:
49
+ the user has a screen for exactly that, with both sides in front of them, and a
50
+ conflict is a question about intent rather than a mechanical failure. Say which
51
+ branch stopped it and what it collided with.
52
+
53
+ Taking one side of a conflicted file *when the user has asked you to* is
54
+ ordinary work and is not gated — `git checkout --ours -- <path>` moves no ref.
55
+ Switching branches still is.
56
+
57
+ ## Do not do this with git
58
+
59
+ Running `git merge` in `api.bash` reaches the same commits and skips everything
60
+ that makes the operation safe: the per-repository lock that stops two windows
61
+ landing at once, the refusal when either branch has moved since it was looked
62
+ at, the sibling-overlap warning, and the accounting write that clears the
63
+ child's row in the user's queue and lets its branch be deleted afterwards. A
64
+ repository merged that way looks right and has no record of what happened to
65
+ it.
66
+
67
+ So when a landing is what is wanted: say so, and let the user run the wizard.
68
+
69
+ ## See also
70
+
71
+ `agent` spawns the children this is about — `isolate=True` is the one that
72
+ gives a child a branch and a worktree of its own. `agent-vs-fork` covers which
73
+ relationship to reach for in the first place.
@@ -0,0 +1,73 @@
1
+ # api.session_nudge(session_id, prompt) -> dict
2
+ <!-- also: nudge, re-prompt, reprompt, wake_child, chase, reuse_subagent, persistent_subagent -->
3
+ <!-- group: Handing work to another agent -->
4
+
5
+ Send a fresh prompt into a child of yours that has gone idle. Requires
6
+ permission; a harness holding the session-spawning grant is allowed it without
7
+ asking.
8
+
9
+ Two jobs, and the second is the one worth planning around.
10
+
11
+ ## Chasing
12
+
13
+ A subagent that stopped without finishing is not lost. It still has everything
14
+ it read, everything it worked out, and whatever it half-did. Name **what is
15
+ outstanding**, in the words you dispatched it with:
16
+
17
+ ```python
18
+ api.session_nudge(child, "The importer is converted but you never said whether "
19
+ "the fixtures still pass. Run them and report.")
20
+ ```
21
+
22
+ "Finish the task" tells it nothing it did not already believe it had done. If
23
+ you dispatched it with `must=[…]`, those obligations are the words to use — and
24
+ when a child ends, the notice you are served names them for you.
25
+
26
+ A subagent that stopped is usually not being lazy. It hit something: a decision
27
+ it could not make, a test it could not run, a dependency that is not there. Ask
28
+ what stopped it as well as telling it to carry on — and if the answer is a
29
+ decision, that is not something to nudge. That is something to take to the user.
30
+
31
+ ## Reuse
32
+
33
+ The expensive part of a subagent is not the subagent. It is the reading: the
34
+ files it had to open before it could say anything useful. A child that has
35
+ already read the importer is the cheapest possible place to send your next
36
+ question about the importer, because it is not going to read it again.
37
+
38
+ ```python
39
+ for row in await api.children():
40
+ if row["nudgeable"] and "importer" in (row["asked"] or ""):
41
+ await api.session_nudge(row["sessionId"], "What does csv.ts do with "
42
+ "newlines inside a quoted field?")
43
+ break
44
+ ```
45
+
46
+ `api.children()` is the roster. Match on **`asked`** — what that child was
47
+ originally sent to do — and not on `label`, which is overwritten with the branch
48
+ name and so reads the same for every child sharing a worktree.
49
+
50
+ This matters most to an agent that cannot read anything itself. If every
51
+ question costs a fresh subagent that starts from an empty head, delegating is a
52
+ tax on curiosity; if it costs a re-prompt, it is just how you work.
53
+
54
+ ## What comes back
55
+
56
+ `{delivered: True}`, or `{delivered: False, reason: "..."}`.
57
+
58
+ A refusal is an ordinary answer rather than an exception, because the commonest
59
+ one — *"it is still working"* — is a normal thing to find when you look, and
60
+ walking a roster should not mean a `try` around every row.
61
+
62
+ | Refused when | Because |
63
+ |---|---|
64
+ | it is mid-turn | A nudge reacts to a session having *stopped*. It is never queued: one arriving mid-sentence reads as an answer to something nobody asked. |
65
+ | it is not your descendant | Same subtree rule as `session_terminate`. You cannot reach a parent, a sibling or a stranger. |
66
+ | it is you | A session prompting itself is a loop with no exit. |
67
+ | the prompt is empty | It would say nothing. |
68
+
69
+ ## It is not `api.agent`
70
+
71
+ `agent` makes a new one. This talks to one you already have. When you find
72
+ yourself about to spawn a second reader for a subject you already have a reader
73
+ for, this is the call you wanted.