@elu-labs/elu 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +327 -2
- package/SECURITY.md +333 -0
- package/THIRD_PARTY_NOTICES.md +62 -0
- package/bin/elu-code.mjs +209 -0
- package/bin/elu.cjs +10 -0
- package/bin/start.cjs +6 -0
- package/docs/peer-network.md +257 -0
- package/docs/permissions.md +285 -0
- package/docs/vendors/antigravity.md +131 -0
- package/docs/vendors/claude.md +124 -0
- package/docs/vendors/codex.md +64 -0
- package/docs/vendors/grok.md +151 -0
- package/lib/acp-client.mjs +100 -0
- package/lib/adapters/agy.mjs +283 -0
- package/lib/adapters/claude.mjs +311 -0
- package/lib/adapters/codex.mjs +137 -0
- package/lib/adapters/grok.mjs +384 -0
- package/lib/adapters/index.mjs +13 -0
- package/lib/agents.mjs +77 -0
- package/lib/agy-hook.mjs +53 -0
- package/lib/approval-server.mjs +85 -0
- package/lib/approvals.mjs +128 -0
- package/lib/awake.mjs +45 -0
- package/lib/board.mjs +261 -0
- package/lib/bot-models.mjs +71 -0
- package/lib/bot-schedules.mjs +291 -0
- package/lib/bots.mjs +361 -0
- package/lib/changes.mjs +168 -0
- package/lib/checks.mjs +102 -0
- package/lib/conductor.mjs +337 -0
- package/lib/config.mjs +147 -0
- package/lib/conformance.mjs +109 -0
- package/lib/default-bots.mjs +150 -0
- package/lib/detect.mjs +108 -0
- package/lib/diff.mjs +217 -0
- package/lib/editor.mjs +165 -0
- package/lib/format.mjs +51 -0
- package/lib/git.mjs +173 -0
- package/lib/init.mjs +129 -0
- package/lib/install.mjs +130 -0
- package/lib/kanban.mjs +180 -0
- package/lib/mcp.mjs +79 -0
- package/lib/memory.mjs +35 -0
- package/lib/models.mjs +144 -0
- package/lib/modes.mjs +95 -0
- package/lib/never-list.mjs +110 -0
- package/lib/never-shims.mjs +311 -0
- package/lib/paste.mjs +56 -0
- package/lib/peer/a2a-http.mjs +166 -0
- package/lib/peer/a2a.mjs +357 -0
- package/lib/peer/asks.mjs +329 -0
- package/lib/peer/canonical.mjs +72 -0
- package/lib/peer/cli.mjs +154 -0
- package/lib/peer/daemon.mjs +525 -0
- package/lib/peer/identity.mjs +81 -0
- package/lib/peer/index.mjs +291 -0
- package/lib/peer/intake.mjs +207 -0
- package/lib/peer/invite.mjs +76 -0
- package/lib/peer/paths.mjs +7 -0
- package/lib/peer/peerdata.mjs +21 -0
- package/lib/peer/pictures.mjs +295 -0
- package/lib/peer/plans.mjs +182 -0
- package/lib/peer/relay.mjs +31 -0
- package/lib/peer/replies.mjs +128 -0
- package/lib/peer/roster.mjs +256 -0
- package/lib/peer/rpc.mjs +88 -0
- package/lib/peer/secrets.mjs +88 -0
- package/lib/peer/settings.mjs +45 -0
- package/lib/peer/shares.mjs +120 -0
- package/lib/peer/status.mjs +84 -0
- package/lib/peer/taskstore.mjs +108 -0
- package/lib/peer/transport.mjs +120 -0
- package/lib/pictures.mjs +104 -0
- package/lib/plan.mjs +238 -0
- package/lib/policy.mjs +98 -0
- package/lib/process.mjs +92 -0
- package/lib/project-settings.mjs +55 -0
- package/lib/project-trust.mjs +230 -0
- package/lib/projects.mjs +668 -0
- package/lib/publish.mjs +105 -0
- package/lib/runner.mjs +874 -0
- package/lib/sandbox.mjs +102 -0
- package/lib/schedules.mjs +270 -0
- package/lib/selection.mjs +202 -0
- package/lib/shell.mjs +101 -0
- package/lib/state.mjs +247 -0
- package/lib/terminal-bots.mjs +239 -0
- package/lib/terminal-commands.mjs +165 -0
- package/lib/terminal-plan.mjs +37 -0
- package/lib/terminal-visuals.mjs +268 -0
- package/lib/terminal.mjs +3159 -0
- package/lib/unit-file.mjs +17 -0
- package/lib/usage.mjs +249 -0
- package/lib/util.mjs +86 -0
- package/lib/vendor-check.mjs +96 -0
- package/lib/vendors.mjs +191 -0
- package/lib/workspace.mjs +168 -0
- package/lib/yaml.mjs +624 -0
- package/npm-shrinkwrap.json +827 -0
- package/package.json +70 -4
package/SECURITY.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Report it privately. Open the repository's **Security** tab and choose **Report a
|
|
6
|
+
vulnerability**, or go straight to
|
|
7
|
+
<https://github.com/ELU-Labs/elu/security/advisories/new>. Please do not open a
|
|
8
|
+
public issue or post reproduction details before a fix is out.
|
|
9
|
+
|
|
10
|
+
Say what you ran: the output of `elu --version`, your operating system,
|
|
11
|
+
the vendor command line and its release, the session's permission mode, and the steps.
|
|
12
|
+
Fixes are made on `main`; there are no older versions that get fixes.
|
|
13
|
+
|
|
14
|
+
## What is trusted
|
|
15
|
+
|
|
16
|
+
ELU Code launches installed vendor CLIs. The executables and your local installation are
|
|
17
|
+
trusted. Repository content, check scripts, prompts, and model responses are untrusted.
|
|
18
|
+
Only the owner can approve a push.
|
|
19
|
+
|
|
20
|
+
Prompts explain the task but are not security controls. This tool cannot prove a change is
|
|
21
|
+
correct, and model review is not a substitute for a person reviewing a pull request.
|
|
22
|
+
|
|
23
|
+
## What an agent may do
|
|
24
|
+
|
|
25
|
+
It depends on the session's permission mode ([docs/permissions.md](docs/permissions.md)).
|
|
26
|
+
|
|
27
|
+
- **Read only** is the profile of every reviewer, every teammate question and every task
|
|
28
|
+
saved without a mode. It gives no shell tools unless the Claude role declares explicit
|
|
29
|
+
command prefixes. ELU Code adds none by itself: the test and build scripts it finds in a
|
|
30
|
+
project run only as checks, inside the check sandbox, and a prefix reaches the model only
|
|
31
|
+
from the project's settings file (`roles.lead.commands`). Ordinary questions in Read only
|
|
32
|
+
have only read-only file tools, with no shell, workspace creation, or check execution.
|
|
33
|
+
- **Ask for approval** and **Approve for me** give the session's model, and a bot that
|
|
34
|
+
works in the session, a shell inside the vendor's own sandbox, with no network. In Ask
|
|
35
|
+
for approval an action outside it asks you where the vendor can ask during a turn
|
|
36
|
+
(Claude Code, and Antigravity and Grok Build once their one-time check has proven it);
|
|
37
|
+
on Codex, which cannot, it is refused and reported. In Approve for me a reviewer model
|
|
38
|
+
decides instead of you. The sandbox differs by vendor: Antigravity in Ask for approval
|
|
39
|
+
has none, so every command needs your approval and one you approve runs outside any
|
|
40
|
+
sandbox, and Grok Build's sandbox blocks the network on Linux only. The vendor pages in
|
|
41
|
+
[docs/vendors](docs/vendors) have the details. In both modes your session's answer
|
|
42
|
+
turns, and a working bot's, also read your home folder without asking, except the
|
|
43
|
+
secret folders ([docs/permissions.md](docs/permissions.md)).
|
|
44
|
+
- **Full access** gives an unsandboxed shell. A Full access shell can read anything your
|
|
45
|
+
user can, except where the vendor's deny rules cover a file tool.
|
|
46
|
+
|
|
47
|
+
The helpers a bot hands work to run under the same mode and the same denied commands as
|
|
48
|
+
the bot.
|
|
49
|
+
|
|
50
|
+
A teammate's question starts only after you accept it, unless you set that teammate to be
|
|
51
|
+
accepted without asking. Its answer then goes back to them without waiting for you to read
|
|
52
|
+
it, unless you chose to read that teammate's replies first. A reply that looks like it
|
|
53
|
+
holds a key or a token is always held for you; that check looks for the shapes of secrets
|
|
54
|
+
and is not a guarantee. Only a project's own folder can be shared, never your home folder,
|
|
55
|
+
and a shared project answers only the teams it was shared with.
|
|
56
|
+
|
|
57
|
+
A picture goes to a teammate only with a message you typed yourself that names it. A path
|
|
58
|
+
in text you pasted into the message counts the same as one you typed, and the line that
|
|
59
|
+
says how many pictures went (`with 2 pictures`) comes after the message is sent. A model's
|
|
60
|
+
draft never attaches a file, and a picture in a folder that holds secrets is refused; a
|
|
61
|
+
hard link in another folder to a file in such a folder is not recognized, and is attached.
|
|
62
|
+
"A picture" means a file whose first bytes say PNG, JPEG, GIF or WebP: the rest of the file
|
|
63
|
+
is not checked, and it is sent as it is, with what it records about the place, the device
|
|
64
|
+
and the time. A picture a teammate sends you is kept in ELU Code's private folder under a
|
|
65
|
+
name made here, readable only by you, and is never run. When you accept their question, its
|
|
66
|
+
run can read the shared project and a copy of those pictures, and nothing else. The copy
|
|
67
|
+
sits in the temporary directory, closed to other users, until the run ends; another session
|
|
68
|
+
of yours that can read the temporary directory could read it in that time.
|
|
69
|
+
|
|
70
|
+
Only Full access uses a vendor permission bypass, and only for the model of a session you
|
|
71
|
+
gave it and the bots that work in that session; every other launcher asks or denies.
|
|
72
|
+
Unsupported policies stop before a role launches. In Read only, Ask for approval and
|
|
73
|
+
Approve for me, task metadata and source checks stay outside agent write access: the file
|
|
74
|
+
tools and the sandboxed shell may not change the repository's Git folder or the protected
|
|
75
|
+
project files, and may not read ELU Code's records in that folder; a command that leaves
|
|
76
|
+
the sandbox asks first. Full access can change them, as it can change anything your user
|
|
77
|
+
can.
|
|
78
|
+
|
|
79
|
+
A sandboxed command reads the rest of the Git folder, so `git status`, `git diff` and
|
|
80
|
+
`git log` work without an approval. Through git it can read the repository's whole
|
|
81
|
+
history: every committed version of every file, including a file committed once and later
|
|
82
|
+
removed, and committed files the sandbox hides by name in the worktree (`.env*`, `*.pem`,
|
|
83
|
+
`.npmrc`). It can read the Git settings file, with its remote URLs and any token written
|
|
84
|
+
into one, and the stash and the reflog. The sandbox has no network, so a sandboxed
|
|
85
|
+
command cannot send what it reads anywhere; what it prints goes to the model, as all
|
|
86
|
+
output does, and into the session's record. `git add`, `git commit` and anything else
|
|
87
|
+
that writes in the folder stay refused. A teammate's question is given tracked files
|
|
88
|
+
only: its commands read none of the folder.
|
|
89
|
+
|
|
90
|
+
Vendor tool permissions restrict direct file tools. Claude Code's own sandbox fences only
|
|
91
|
+
shell commands and is never required: where it cannot start, commands run outside it under
|
|
92
|
+
the same rules (Ask asks you for each, a reviewer model decides in Approve for me, a Read
|
|
93
|
+
only role keeps its command prefixes).
|
|
94
|
+
|
|
95
|
+
Claude shell roles are labelled "temp folder writable": shared temp directories are denied,
|
|
96
|
+
but the vendor can retain its own writable temporary folder outside the worktree. This
|
|
97
|
+
exception is visible in `elu policy show`. In Read only, Ask for approval and Approve for
|
|
98
|
+
me ELU Code names that folder itself where its path is short enough for Claude Code to
|
|
99
|
+
use: a private one under your cache folder, or in your runtime folder where that path is
|
|
100
|
+
too long, made for the run and removed when it ends (`docs/vendors/claude.md`).
|
|
101
|
+
|
|
102
|
+
## What teammates are told
|
|
103
|
+
|
|
104
|
+
A project is shared with `elu share` or `/share`, and also when a teammate is added:
|
|
105
|
+
`elu invite` and `elu join` share the folder they run in when it is an ELU Code project
|
|
106
|
+
(`--project PATH` names another), and `/invite` and `/join` share the open project. A share has status
|
|
107
|
+
on unless you made it with `elu share --no-status`; `/share`, and `elu share` without that
|
|
108
|
+
flag, turn status on again. While status is on, and the project has an `origin` remote, the
|
|
109
|
+
teammates on a team it is shared with are told two things about it, and neither is checked
|
|
110
|
+
for secrets:
|
|
111
|
+
|
|
112
|
+
- **Status rows**, one for each live editing task (one being prepared, running, ready or
|
|
113
|
+
waiting for you; a plain conversation has none): the first line of the request it works
|
|
114
|
+
on, up to 80 characters; its state and what is at work on it (`lead`, `checks`, a shared
|
|
115
|
+
bot's `@name`, or `bot` for another bot); and the paths, relative to the repository, of up
|
|
116
|
+
to 200 files it has changed or added. They are used to warn about overlapping work. The
|
|
117
|
+
peer daemon reads them every 10 seconds and sends them when they change, and to each
|
|
118
|
+
teammate when they connect.
|
|
119
|
+
- **Tasks**: for each session, the approved Task its work is on and any finished in the
|
|
120
|
+
last 24 hours, with the title, description, goal, objectives, priority, severity, state,
|
|
121
|
+
subtasks, which objectives are met, who works on it, and its times. They are sent within
|
|
122
|
+
5 seconds of a change, and to each teammate when they connect.
|
|
123
|
+
|
|
124
|
+
Every teammate on any of your teams is also told, whatever the status of your shares, this
|
|
125
|
+
machine's name (the peer setting `box`, by default the first part of its host name), the
|
|
126
|
+
bots you share and what each is for, and the names of the projects shared with their team.
|
|
127
|
+
Nothing else about your work is told: not a Task you have not approved or a rejected one,
|
|
128
|
+
the rest of your request, the chat, file contents or absolute paths. A teammate's question
|
|
129
|
+
and its answer are another matter, covered above.
|
|
130
|
+
|
|
131
|
+
What a teammate tells is untrusted text: each field is checked and cut to its limit before
|
|
132
|
+
it is kept, and it counts only in a project you share with a team that teammate is on. Your
|
|
133
|
+
own model and bots in that project are given the titles and goals as data, marked as a
|
|
134
|
+
teammate's text and never as a request; a teammate's question running on your machine is
|
|
135
|
+
given none of it.
|
|
136
|
+
|
|
137
|
+
## The never list
|
|
138
|
+
|
|
139
|
+
The never list (push, tag, merge, deploy, publish) is enforced only where the means are
|
|
140
|
+
missing: Read only and Ask for approval, with the one exception below. In Approve for me
|
|
141
|
+
and Full access it is prompt text plus the vendor's deny rules, which match command text
|
|
142
|
+
and can be sidestepped by an obscured command. ELU Code's own publish step still needs
|
|
143
|
+
your confirmation and still refuses a default branch in every mode.
|
|
144
|
+
|
|
145
|
+
One entry can open. A bot that is allowed to merge pull requests (`@elu` comes allowed; a
|
|
146
|
+
bot you make is not) may run `gh pr merge` in its own working turn on Claude Code: its deny
|
|
147
|
+
rules leave that command out, and its `gh` stand-in lets it through. In Ask for approval
|
|
148
|
+
you are asked before each merge. In Approve for me the reviewer model decides and you are
|
|
149
|
+
not asked, and Full access does not ask. A turn its schedule started runs with Approve for
|
|
150
|
+
me at most, so in a session set to Approve for me or Full access the hourly `@elu` can
|
|
151
|
+
merge with nobody watching. Push, tag, `git merge`, deploy and publish stay blocked for
|
|
152
|
+
that bot as for every other.
|
|
153
|
+
|
|
154
|
+
A rule on command text does not see what a script runs, and Codex has no deny rules at
|
|
155
|
+
all. So every vendor's session, in every mode but Read only, also gets stand-in commands:
|
|
156
|
+
a private folder (`~/.local/share/elu-code/never-list`, mode 0700) goes first on the
|
|
157
|
+
session's PATH with one script per command word, and each refuses the never-list call by
|
|
158
|
+
name whatever runs it, a script or `sh -c` included. Claude Code builds PATH again for
|
|
159
|
+
each command, so ELU Code names a one-line file in `CLAUDE_ENV_FILE` that puts the folder
|
|
160
|
+
back in front. This was seen with the real Codex and the real Claude Code in Ask for
|
|
161
|
+
approval and Approve for me, not in Full access; with Antigravity and Grok Build only in
|
|
162
|
+
tests against stand-in tools. They are a tripwire as well, not a sandbox. These get
|
|
163
|
+
past them: a full path to the real command (`/usr/bin/git push`), a changed PATH, a Git
|
|
164
|
+
alias (`git -c alias.p=push p`) and another program that does the same work (`gh api`,
|
|
165
|
+
`git send-pack`). Two things pass on purpose, because they are not publishing: a
|
|
166
|
+
`git push` to a folder on this machine (a path or a `file://` URL, as git itself resolves
|
|
167
|
+
the remote), and a `git merge` or `git tag` in a scratch repository that lies, links
|
|
168
|
+
resolved, inside the system temporary directory. A push to another machine, or one git
|
|
169
|
+
cannot place, is refused before anything is sent. A project kept under the temporary
|
|
170
|
+
directory is treated as scratch. A session in Full access can also rewrite the folder; it is written
|
|
171
|
+
again, and cleared of anything else, when the next session starts. In Ask for approval
|
|
172
|
+
and Approve for me the vendor's sandbox, where it runs one, still stands behind them; in
|
|
173
|
+
Full access nothing does.
|
|
174
|
+
|
|
175
|
+
## Approvals
|
|
176
|
+
|
|
177
|
+
Approvals have limits. They are a convenience, not a boundary against an agent that is
|
|
178
|
+
already outside the sandbox: a process running as your user can forge an answer file. Ask
|
|
179
|
+
for approval keeps the agent inside the sandbox until you say yes; Approve for me trusts
|
|
180
|
+
the vendor's reviewer model; Full access trusts your choice. Request text is model output:
|
|
181
|
+
control characters are stripped, otherwise the command, path or URL is shown exactly.
|
|
182
|
+
|
|
183
|
+
## Configuration, MCP servers and hooks
|
|
184
|
+
|
|
185
|
+
A project's own settings and instructions (`.elu-code/config.yaml`, `instructions.md` and
|
|
186
|
+
`context.md`) come with its folder, so they count as yours only once you have approved
|
|
187
|
+
their exact content on this machine. ELU Code shows a file that is new or changed and asks
|
|
188
|
+
once whether to use it. Until you approve a first version, sessions run on the default
|
|
189
|
+
settings and without those instructions. After that, the version you approved last stays in
|
|
190
|
+
use until you approve a change: a file that changed, that you turned down or that was
|
|
191
|
+
removed does not put the defaults back. A file over 100 KB is never used. Scripted runs
|
|
192
|
+
are refused while the settings file in the folder is not one you approved. Your answers are
|
|
193
|
+
kept by content hash, with the text you approved last, in ELU Code's own state folder,
|
|
194
|
+
never in the project. Like approvals, this is not a boundary against a process that already
|
|
195
|
+
runs as your user: a Full access session can write that folder, and so can a command that
|
|
196
|
+
was approved to leave the sandbox in Ask for approval or in Approve for me, where the
|
|
197
|
+
approver is the vendor's reviewer model. With a Codex model, the repository's own
|
|
198
|
+
`AGENTS.md` reaches the model as Codex itself reads it, and this step does not cover it.
|
|
199
|
+
|
|
200
|
+
ELU Code starts one MCP server of its own, the approval relay, and only for a session with
|
|
201
|
+
permissions: a dependency-free process that turns Claude Code's permission prompts into
|
|
202
|
+
requests you answer in the terminal. Its Claude launcher otherwise uses strict MCP
|
|
203
|
+
configuration, holding only the servers ELU Code passes, and loads no settings files
|
|
204
|
+
(`--setting-sources ''`). The Codex reviewer ignores user configuration and refuses
|
|
205
|
+
project or managed configuration that has not passed isolation tests.
|
|
206
|
+
|
|
207
|
+
Your own Claude Code MCP servers (`claude mcp list`: the ones in `~/.claude.json`, for all
|
|
208
|
+
folders and for the project folder) are given to the model of your sessions on Claude Code
|
|
209
|
+
and to each bot that works there, unless you turn one off for the project with
|
|
210
|
+
`/mcp off NAME`. A bot also gets the servers its page names, even one turned off for the
|
|
211
|
+
project. Bots that only review or answer, a read-only turn and a teammate's question get
|
|
212
|
+
none. Their tools run without asking in every mode: in Read only (a task saved without a
|
|
213
|
+
mode), Ask for approval and Approve for me they are allowed by name, and Full access
|
|
214
|
+
allows everything. They run as their own processes, so neither the shell sandbox nor the
|
|
215
|
+
never list covers them: a server that can push, merge, post or send mail can do so
|
|
216
|
+
without asking you. Their settings, which can hold tokens, are copied for each run into a
|
|
217
|
+
private file in ELU Code's state folder and removed when the run ends.
|
|
218
|
+
|
|
219
|
+
On Claude Code, the model of your sessions and each bot that works there also get your
|
|
220
|
+
Claude Code instructions and memory for the project folder, as Claude Code itself would
|
|
221
|
+
load them: `~/.claude/CLAUDE.md`, the folder's `CLAUDE.md`, and the notes in
|
|
222
|
+
`~/.claude/projects/<folder>/memory`. The two instruction files and the memory index go
|
|
223
|
+
into the prompt of a session's first turn; the notes are copied for each run into a
|
|
224
|
+
private folder in the system temporary directory, which the run may read, and removed when
|
|
225
|
+
it ends. A teammate's question gets none of them. The folder's `CLAUDE.md` comes with the
|
|
226
|
+
folder, and the approval step above does not cover it.
|
|
227
|
+
|
|
228
|
+
Hooks are inspected before editing. Configured hook folders in the task worktree are
|
|
229
|
+
protected, and a changed hook stops checks and publication. Publishing uses an absolute
|
|
230
|
+
private copy of the reviewed hooks. It runs the existing pre-push guards, including the
|
|
231
|
+
guard in Git's common directory; commit hooks are not run. Git preparation and staging do
|
|
232
|
+
not execute checkout or index hooks. Push guards run with a read-only worktree, private
|
|
233
|
+
temporary space, and network access denied. A hook that invokes edited source inherits
|
|
234
|
+
these same limits. The original hook files and Git configuration are unchanged. Git
|
|
235
|
+
attributes and submodules that could execute repository code are refused.
|
|
236
|
+
|
|
237
|
+
Terminal requests pin a hashed policy in private task state; scripted runs pin
|
|
238
|
+
configuration and commands to a base commit. A session's mode, place and folders live in
|
|
239
|
+
that private state, never in repository files, and changing the mode between turns changes
|
|
240
|
+
no policy.
|
|
241
|
+
|
|
242
|
+
## Credentials
|
|
243
|
+
|
|
244
|
+
Agents do not receive cloud or GitHub credentials from ELU Code's inherited environment.
|
|
245
|
+
Vendor authentication remains the vendor CLI's responsibility. ELU Code never signs you in.
|
|
246
|
+
It never reads Claude Code's sign-in, in the macOS Keychain or in its credentials file, and
|
|
247
|
+
it sends no request of its own with any vendor sign-in.
|
|
248
|
+
|
|
249
|
+
The Usage tab shows plan usage. Claude usage comes from the rate limit lines Claude Code
|
|
250
|
+
writes to its own output during a session that ELU Code runs. No request is made for it,
|
|
251
|
+
and until such a session has run, the tab says so.
|
|
252
|
+
|
|
253
|
+
Codex usage comes from Codex's own `app-server` command: ELU Code starts it and asks it for
|
|
254
|
+
your plan's rate limits, so the request with your Codex sign-in is made by Codex. It starts
|
|
255
|
+
in an empty private folder with MCP servers, hooks, apps, plugins and analytics turned off,
|
|
256
|
+
and no thread or turn is ever started. ELU Code asks for this check whenever `codex` is on
|
|
257
|
+
your PATH, including when you have it only for the check sandbox:
|
|
258
|
+
|
|
259
|
+
- when you open a project that is set up, unless another ELU Code window already has it
|
|
260
|
+
open, and when you finish setting one up;
|
|
261
|
+
- when you start a session or send a message;
|
|
262
|
+
- every minute while a session, or the new session form, is on screen;
|
|
263
|
+
- when the Usage tab opens and its last check is more than two minutes old, and when you
|
|
264
|
+
press `r` there.
|
|
265
|
+
|
|
266
|
+
Apart from `r`, a check asked for less than a minute after the last one is skipped.
|
|
267
|
+
|
|
268
|
+
What is saved, in `~/.config/elu-code`, is usage reduced to numbers and times
|
|
269
|
+
(`usage.json`), and the email and plan name of each account seen, with its last usage
|
|
270
|
+
(`accounts.json`). The Usage tab takes the Claude email, plan name and rate limit tier from
|
|
271
|
+
`~/.claude.json`. It decodes the Codex email and plan on your machine from the id token in
|
|
272
|
+
`auth.json` in the Codex home. No token from that file is sent, saved or shown.
|
|
273
|
+
|
|
274
|
+
## Checks and evidence
|
|
275
|
+
|
|
276
|
+
Check scripts and pre-push guards run inside Codex's OS sandbox (`codex sandbox`, or the
|
|
277
|
+
copy `ELU_CODE_SANDBOX_HELPER` names), with an empty Codex home of their own, so Codex must
|
|
278
|
+
be installed whatever vendor your sessions use, but need not be signed in. A check that
|
|
279
|
+
cannot start in the sandbox fails; it never retries without isolation. Runtime directories
|
|
280
|
+
needed by installed check programs are readable, not writable.
|
|
281
|
+
|
|
282
|
+
Checks have a fresh private temporary directory inside the task worktree. Shared system
|
|
283
|
+
temporary directories are denied. Ignored output is included in the evidence digest and
|
|
284
|
+
blocks completion because it would be absent from the commit. Build and test scripts must
|
|
285
|
+
not depend on unpublished generated files.
|
|
286
|
+
|
|
287
|
+
Missing project checks leave code unverified and cannot authorize publication. Source
|
|
288
|
+
edits after a check or review invalidate that evidence. Changing an allowed test to assert
|
|
289
|
+
less is still possible and must be caught in review.
|
|
290
|
+
|
|
291
|
+
## Output and secrets
|
|
292
|
+
|
|
293
|
+
Raw output can contain secrets. It remains in `.git/elu-code/` (or personal conversation
|
|
294
|
+
state before a folder has Git history), never in the PR evidence block. Review responses
|
|
295
|
+
get conservative pattern-based redaction, which cannot recognize all personal or business
|
|
296
|
+
information. Inspect the exact body before approving publication.
|
|
297
|
+
|
|
298
|
+
## What the tests prove
|
|
299
|
+
|
|
300
|
+
Compiler and fake-CLI tests establish ELU Code's behavior. Sandbox probes establish the
|
|
301
|
+
installed helper's boundaries. Actual agent enforcement requires live tests on the exact
|
|
302
|
+
vendor release, including commands launched by a test script, outside writes, synthetic
|
|
303
|
+
secret reads, network requests, protected edits, configured MCP servers, and cancellation.
|
|
304
|
+
A model choosing not to attempt an action is not proof that the vendor refused it.
|
|
305
|
+
|
|
306
|
+
Use a tested vendor release for sensitive repositories. Operating system updates, managed
|
|
307
|
+
settings, and vendor updates can change enforcement.
|
|
308
|
+
|
|
309
|
+
## Linux
|
|
310
|
+
|
|
311
|
+
A write outside the mounted files may land in a private, temporary filesystem without an
|
|
312
|
+
error. Sandbox probes and tests verify that the real files remain unchanged. Keep
|
|
313
|
+
repositories with push hooks outside shared `/tmp` and `/var/tmp` folders: the tested
|
|
314
|
+
helper cannot mount read-only hook inputs there.
|
|
315
|
+
|
|
316
|
+
## Antigravity and Grok Build
|
|
317
|
+
|
|
318
|
+
Antigravity CLI runs under a private HOME holding only ELU Code's settings and hook; its
|
|
319
|
+
rules and that hook were designed from Google's documentation, and the steps of its
|
|
320
|
+
one-time check, which ELU Code runs before your first message to it, are the only proof on
|
|
321
|
+
a given machine. Google's FAQ says that using third-party software to access Antigravity
|
|
322
|
+
violates its terms and can get the account suspended or terminated, and that wrapping
|
|
323
|
+
`agy` in other applications is not supported on consumer plans. ELU Code wraps it: using
|
|
324
|
+
it is the owner's decision, and the owner's account carries the risk. Antigravity's deny
|
|
325
|
+
rules and hook match command text: in Approve for me and Full access the never list is a
|
|
326
|
+
tripwire, not a boundary.
|
|
327
|
+
|
|
328
|
+
Grok Build runs with a private HOME and every documented Claude/Cursor compatibility,
|
|
329
|
+
telemetry and upload switch off; the `isolation` step of its one-time check is the only
|
|
330
|
+
proof on a given machine, and the vendor's own issue
|
|
331
|
+
[xai-org/grok-build-plugin-cc#34](https://github.com/xai-org/grok-build-plugin-cc/issues/34)
|
|
332
|
+
says some switches are ignored. Grok's deny rules match command text: in Approve for me
|
|
333
|
+
and Full access the never list is a tripwire, not a boundary.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Two of the bots ELU Code comes with are adapted from skills published by others. Their
|
|
4
|
+
instructions are in `lib/default-bots.mjs`. Some test files are examples copied from a
|
|
5
|
+
published specification; they are in the repository only, not in the npm package.
|
|
6
|
+
|
|
7
|
+
## @reviewer
|
|
8
|
+
|
|
9
|
+
Adapted from the `code-review` skill of the Engineering plugin in
|
|
10
|
+
[anthropics/knowledge-work-plugins](https://github.com/anthropics/knowledge-work-plugins)
|
|
11
|
+
(`engineering/skills/code-review/SKILL.md`, version 1.2.0), by Anthropic, licensed under the
|
|
12
|
+
Apache License, Version 2.0. A copy of that license is in
|
|
13
|
+
[LICENSES/Apache-2.0.txt](LICENSES/Apache-2.0.txt).
|
|
14
|
+
|
|
15
|
+
Changes: the review dimensions (security, performance, correctness, maintainability) and the
|
|
16
|
+
shape of the answer (summary, critical issues, suggestions, what looks good, verdict) are
|
|
17
|
+
kept. The text is rewritten for a bot that reviews the change of a session and reports its
|
|
18
|
+
findings as blocking or optional comments. The slash command, the connector notes and the
|
|
19
|
+
usage tips are left out.
|
|
20
|
+
|
|
21
|
+
## @qa
|
|
22
|
+
|
|
23
|
+
Adapted from the `qa` skill of [garrytan/gstack](https://github.com/garrytan/gstack)
|
|
24
|
+
(`qa/SKILL.md`), licensed under the MIT License. The method is kept: test, report a
|
|
25
|
+
baseline by severity, then for each finding reproduce it, write the regression test first,
|
|
26
|
+
make the smallest fix, test again and classify the result, with rules for when to stop. The
|
|
27
|
+
text is rewritten for a bot in a session; the browser tooling, the setup steps, the report
|
|
28
|
+
files and the telemetry of the original are left out.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
MIT License
|
|
32
|
+
|
|
33
|
+
Copyright (c) 2026 Garry Tan
|
|
34
|
+
|
|
35
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
36
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
37
|
+
in the Software without restriction, including without limitation the rights
|
|
38
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
39
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
40
|
+
furnished to do so, subject to the following conditions:
|
|
41
|
+
|
|
42
|
+
The above copyright notice and this permission notice shall be included in all
|
|
43
|
+
copies or substantial portions of the Software.
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
47
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
48
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
49
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
50
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
51
|
+
SOFTWARE.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## A2A specification examples
|
|
55
|
+
|
|
56
|
+
The six files in `test/fixtures/a2a/` are examples from the Agent2Agent (A2A) Protocol
|
|
57
|
+
specification in [a2aproject/A2A](https://github.com/a2aproject/A2A)
|
|
58
|
+
(`docs/specification.md`, tag `v1.0.1`), licensed under the Apache License, Version 2.0. A
|
|
59
|
+
copy of that license is in [LICENSES/Apache-2.0.txt](LICENSES/Apache-2.0.txt).
|
|
60
|
+
|
|
61
|
+
Changes: each file holds one example, as JSON, with a `_source` field added that names the
|
|
62
|
+
section of the specification and the lines it comes from.
|
package/bin/elu-code.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { repository, findRepository } from '../lib/git.mjs';
|
|
7
|
+
import { initPlan, applyInit, uninstallPlan, applyUninstall } from '../lib/init.mjs';
|
|
8
|
+
import { runPlan, runTask } from '../lib/runner.mjs';
|
|
9
|
+
import { readTask, saveTask, taskDir, tasks, lock } from '../lib/state.mjs';
|
|
10
|
+
import { runChecks, pinnedConfig } from '../lib/checks.mjs';
|
|
11
|
+
import { policyReport, assertPolicy } from '../lib/policy.mjs';
|
|
12
|
+
import { unsupportedVendors } from '../lib/detect.mjs';
|
|
13
|
+
import { ADAPTERS } from '../lib/adapters/index.mjs';
|
|
14
|
+
import { preparePublish, publish, prBody } from '../lib/publish.mjs';
|
|
15
|
+
import { refuse, quote, readJson } from '../lib/util.mjs';
|
|
16
|
+
import { recoverProcess, addUserBin } from '../lib/process.mjs';
|
|
17
|
+
import {formatResult}from'../lib/format.mjs';
|
|
18
|
+
import {botKey}from'../lib/bots.mjs';
|
|
19
|
+
import { sweepServers } from '../lib/mcp.mjs';
|
|
20
|
+
|
|
21
|
+
addUserBin();
|
|
22
|
+
// MCP settings a run could not remove because its process died (lib/mcp.mjs).
|
|
23
|
+
sweepServers();
|
|
24
|
+
// Peer commands load lib/peer/ only when used; it never imports the optional packages at load time.
|
|
25
|
+
const PEER_COMMANDS = new Set(['id', 'team', 'invite', 'join', 'share', 'unshare', 'peers', 'peer', 'ask', 'relay']);
|
|
26
|
+
const HELP = `ELU Code — terminal coding workspace\n\nRun elu anywhere to open the terminal app.\nThe first start asks for a project name; later starts open your last project.\nSessions lists every session; type a message to start one.\nKanban shows the open project's tasks. Chats holds your channels and direct messages. Type / for commands and help.\nEditing workspaces are prepared automatically. Pushes require separate approval.\n\nCommands:\n conductor [stop]\n schedule [list]\n dashboard\n init [--yes] [--dry-run]\n run <goal> [--yes] [--dry-run] [--permissions auto|full] [--place here|branch:NAME] [--folder PATH]...\n run <id> --publish [--approve-push <confirmationId>]\n status [id] [--recover]\n open <id> [--bot NAME] [--exec]\n check <id>\n policy show\n uninstall [--yes] [--dry-run]\n\nPeer network:\n id [--name N] [--export] [--import CODE]\n team create NAME | team\n invite [--team ID] [--hours N] [--project PATH]\n join CODE [--project PATH]\n share [PATH] [--as HANDLE] [--no-status] | unshare HANDLE\n peers\n peer on|off|status|install|uninstall\n ask [@NAME] [--project H] TEXT\n relay [--team ID]\n\nEvery command accepts --json. --dry-run prints the proposed task setup;\nit does not execute the task or verify the resulting implementation.\n`;
|
|
27
|
+
function argumentsOf(argv) {
|
|
28
|
+
const flags = {}; const words = [];
|
|
29
|
+
const boolean = new Set(['json', 'yes', 'dry-run', 'publish', 'recover', 'exec', 'help', 'version', 'daemon', 'export', 'no-status']);
|
|
30
|
+
const values = new Set(['role', 'bot', 'approve-push', 'name', 'import', 'team', 'hours', 'as', 'project', 'permissions', 'place']);
|
|
31
|
+
const repeatable = new Set(['folder']);
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
// -h alone or straight after a command; inside a goal or a question it is a word of the text.
|
|
35
|
+
if (arg === '-h' && (i === 0 || (i === 1 && argv.length === 2))) { flags.help = true; continue; }
|
|
36
|
+
if (!arg.startsWith('--')) { words.push(arg); continue; }
|
|
37
|
+
const [name, inline] = arg.slice(2).split('=', 2);
|
|
38
|
+
if (Object.hasOwn(flags, name) && !repeatable.has(name)) refuse(`Duplicate option --${name}.`);
|
|
39
|
+
if (boolean.has(name) && inline === undefined) flags[name] = true;
|
|
40
|
+
else if (values.has(name) || repeatable.has(name)) {
|
|
41
|
+
const value = inline ?? argv[++i];
|
|
42
|
+
if (!value || value.startsWith('--')) refuse(`--${name} needs a value.`);
|
|
43
|
+
if (repeatable.has(name)) (flags[name] ??= []).push(value); else flags[name] = value;
|
|
44
|
+
} else refuse(`Unknown option ${arg}.`);
|
|
45
|
+
}
|
|
46
|
+
return { flags, words };
|
|
47
|
+
}
|
|
48
|
+
async function confirm(plan, flags) {
|
|
49
|
+
if (flags['dry-run']) return false;
|
|
50
|
+
if (flags.yes) return true;
|
|
51
|
+
if (flags.json || !process.stdin.isTTY) return false;
|
|
52
|
+
process.stdout.write(formatResult(plan));
|
|
53
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
54
|
+
const closed=new Promise(resolve=>prompt.once('close',()=>resolve('')));
|
|
55
|
+
try { const answer=await Promise.race([prompt.question('Make these writes? [y/N] ').catch(error=>{if(error.code==='ERR_USE_AFTER_CLOSE'||error.code==='ABORT_ERR')return'';throw error;}),closed]);return /^y(?:es)?$/i.test(answer.trim()); }
|
|
56
|
+
finally { prompt.close(); }
|
|
57
|
+
}
|
|
58
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
59
|
+
const { flags, words } = argumentsOf(argv);
|
|
60
|
+
if (flags.help || words[0] === 'help') return { help: `${HELP}\nOptions:\n --help, -h\n --version\n` };
|
|
61
|
+
if (flags.version) return { version: readJson(fileURLToPath(new URL('../package.json', import.meta.url))).version };
|
|
62
|
+
if (words[0] === 'schedule') {
|
|
63
|
+
const schedules = await import('../lib/schedules.mjs');
|
|
64
|
+
// The login item runs this every minute. It prints only when something happens.
|
|
65
|
+
// Bots that have a schedule are counted with the schedules made with /schedule.
|
|
66
|
+
if (words[1] === 'tick') { const bots = (await import('../lib/bot-schedules.mjs')).runBotSchedules(), fired = bots + schedules.runDue(); (await import('../lib/awake.mjs')).refreshAwake(); return fired ? { message: `Queued ${fired} scheduled task${fired === 1 ? '' : 's'}.` } : { quiet: true }; }
|
|
67
|
+
if (words[1] === 'list' || !words[1]) return { schedules: schedules.listSchedules(), message: schedules.listSchedules().map(row => `${row.enabled ? row.when.label : 'paused'} ${row.project} ${row.goal.split('\n')[0]}${row.nextRunAt ? ` next ${schedules.dateText(new Date(row.nextRunAt))}` : ''}`).join('\n') || 'No schedules. In ELU Code, set When in + New task.' };
|
|
68
|
+
refuse(`Unknown command: schedule ${words[1]}. Use elu schedule [list|tick].`);
|
|
69
|
+
}
|
|
70
|
+
if (words[0] === 'conductor') {
|
|
71
|
+
const conductor = await import('../lib/conductor.mjs');
|
|
72
|
+
if (flags.daemon) {
|
|
73
|
+
const owner = new conductor.Conductor({ onExit: () => close() });
|
|
74
|
+
let closing = false; const close = async () => { if (closing) return; closing = true; await owner.stop(); process.exit(0); };
|
|
75
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, close);
|
|
76
|
+
owner.start(); return new Promise(() => {});
|
|
77
|
+
}
|
|
78
|
+
if (words[1] === 'stop') {
|
|
79
|
+
if (!conductor.conductorAlive()) return { message: 'The conductor is not running.' };
|
|
80
|
+
conductor.sendCommand({ type: 'shutdown' }); return { message: 'The conductor stops its runs, keeps their history, and exits. It starts again when work is queued.' };
|
|
81
|
+
}
|
|
82
|
+
if (words[1]) refuse(`Unknown command: conductor ${words[1]}. Use elu conductor [stop].`);
|
|
83
|
+
if (flags.json || !process.stdin.isTTY || !process.stdout.isTTY) { const { dashboardSnapshot } = await import('../lib/projects.mjs'); const { boardRows } = await import('../lib/board.mjs'); return { conductor: conductor.conductorStatus(), agents: boardRows(dashboardSnapshot().projects).map(({ task, request, key, order, ...row }) => row) }; }
|
|
84
|
+
const { launchTerminal } = await import('../lib/terminal.mjs'); return launchTerminal();
|
|
85
|
+
}
|
|
86
|
+
if (words[0] === 'peer' && flags.daemon) {
|
|
87
|
+
const { PeerDaemon } = await import('../lib/peer/daemon.mjs');
|
|
88
|
+
const owner = new PeerDaemon({ onExit: () => close() });
|
|
89
|
+
let closing = false; const close = async () => { if (closing) return; closing = true; await owner.stop(); process.exit(0); };
|
|
90
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, close);
|
|
91
|
+
await owner.start(); return new Promise(() => {});
|
|
92
|
+
}
|
|
93
|
+
if (words[0] === 'relay') {
|
|
94
|
+
const { startRelay } = await import('../lib/peer/cli.mjs');
|
|
95
|
+
const { relay, result } = await startRelay(flags);
|
|
96
|
+
let closing = false; const close = async () => { if (closing) return; closing = true; await relay.close(); process.exit(0); };
|
|
97
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, close);
|
|
98
|
+
process.stdout.write(flags.json ? `${JSON.stringify({ success: true, ...result })}\n` : formatResult(result));
|
|
99
|
+
return new Promise(() => {});
|
|
100
|
+
}
|
|
101
|
+
if (words[0] === 'team' && !words[1] && !flags.json && process.stdin.isTTY && process.stdout.isTTY) { const { launchTerminal } = await import('../lib/terminal.mjs'); return launchTerminal({ team: true, repo: findRepository(process.cwd()) }); }
|
|
102
|
+
if (PEER_COMMANDS.has(words[0])) { const { peerCli } = await import('../lib/peer/cli.mjs'); return peerCli(words, flags); }
|
|
103
|
+
if (words[0] === 'dashboard') {
|
|
104
|
+
if (flags.json) { const { dashboardSnapshot } = await import('../lib/projects.mjs'); return dashboardSnapshot(); }
|
|
105
|
+
const { launchTerminal } = await import('../lib/terminal.mjs'); return launchTerminal();
|
|
106
|
+
}
|
|
107
|
+
if (!words.length && !flags.json && (!process.stdin.isTTY || !process.stdout.isTTY)) return { help: HELP };
|
|
108
|
+
if (!words.length) {
|
|
109
|
+
const repo = findRepository(process.cwd());
|
|
110
|
+
if (flags.json) { const { projectSnapshot, dashboardSnapshot } = await import('../lib/projects.mjs'); return repo ? projectSnapshot(repo) : { welcome: true, directory: process.cwd(), ...dashboardSnapshot() }; }
|
|
111
|
+
const { launchTerminal } = await import('../lib/terminal.mjs'); return launchTerminal({ repo, startDirectory: process.cwd(), askProject: true });
|
|
112
|
+
}
|
|
113
|
+
const repo = repository(process.cwd());
|
|
114
|
+
const command = words[0];
|
|
115
|
+
if (command === 'init') {
|
|
116
|
+
const plan = initPlan(repo);
|
|
117
|
+
if (plan.initialized) return plan;
|
|
118
|
+
return await confirm(plan, flags) ? applyInit(repo, plan) : { ...plan, needsConfirmation: !flags['dry-run'] };
|
|
119
|
+
}
|
|
120
|
+
if (command === 'policy' && words[1] === 'show') {
|
|
121
|
+
const { head } = await import('../lib/git.mjs');
|
|
122
|
+
const { config } = pinnedConfig(repo, head(repo.root));
|
|
123
|
+
// Only the lead entry runs. Bots do the reviews, read-only, whatever an older config lists.
|
|
124
|
+
const role = config.roles.lead; let refusal = null; try { assertPolicy(role); } catch (e) { refusal = e.message; }
|
|
125
|
+
return { roles: [{ name: 'lead', ...policyReport(role), refusal }], unsupportedVendors: unsupportedVendors().map(({ bin, ...value }) => value) };
|
|
126
|
+
}
|
|
127
|
+
if (command === 'run') {
|
|
128
|
+
if (flags.publish) {
|
|
129
|
+
if (flags.yes || flags['dry-run']) refuse('Publication uses its own exact push confirmation.');
|
|
130
|
+
const task = readTask(repo, words[1]); const release = lock(repo, `task-${task.id}`);
|
|
131
|
+
try { return flags['approve-push'] ? await publish(repo, task, flags['approve-push']) : await preparePublish(repo, task); }
|
|
132
|
+
finally { release(); }
|
|
133
|
+
}
|
|
134
|
+
if (flags['approve-push']) refuse('--approve-push requires --publish.');
|
|
135
|
+
// A scripted run picks a mode too; ask needs someone at the terminal to answer.
|
|
136
|
+
if (flags.permissions === 'ask') refuse('Ask needs the terminal app.');
|
|
137
|
+
const session = { ...(flags.permissions !== undefined ? { permissions: flags.permissions } : {}), ...(flags.place !== undefined ? { place: flags.place } : {}), ...(flags.folder ? { folders: flags.folder.map(dir => path.resolve(dir)) } : {}) };
|
|
138
|
+
const release = lock(repo);
|
|
139
|
+
let plan;
|
|
140
|
+
try { plan = await runPlan(repo, words.slice(1).join(' '), session); } finally { release(); }
|
|
141
|
+
if(flags['dry-run'])return{...plan,preview:true,needsConfirmation:false,message:'Review this task preview. Nothing ran and no model credit was used.'};
|
|
142
|
+
const approved = await confirm(plan, flags);
|
|
143
|
+
if (!approved) return { ...plan, needsConfirmation: true };
|
|
144
|
+
const task = await runTask(repo, plan);
|
|
145
|
+
return { ...task, draftPrBody: prBody(task), published: false, message: 'Local work finished. Use run <id> --publish to review the exact commit and push destination.' };
|
|
146
|
+
}
|
|
147
|
+
if (command === 'status') {
|
|
148
|
+
let values = words[1] ? [readTask(repo, words[1])] : tasks(repo);
|
|
149
|
+
if (flags.recover) {
|
|
150
|
+
values = values.map((task) => {
|
|
151
|
+
const release = lock(repo, `task-${task.id}`);
|
|
152
|
+
try {
|
|
153
|
+
const file = path.join(taskDir(repo, task.id), 'process.json');
|
|
154
|
+
const record = fs.existsSync(file) ? readJson(file) : null;
|
|
155
|
+
if (task.state !== 'running' && !record?.pid) return task;
|
|
156
|
+
if (record) recoverProcess(record);
|
|
157
|
+
task.state = 'blocked'; task.error = { message: 'The controlling process ended. Review the worktree and rerun checks; nothing was restarted.' }; saveTask(repo, task); return task;
|
|
158
|
+
} finally { release(); }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { tasks: values.map(({ goal, runs, responses, reviews, ...task }) => ({ ...task, goal, runs: runs.map(({ logFile, ...run }) => run), responses, reviewRounds: reviews.length })) };
|
|
162
|
+
}
|
|
163
|
+
if (command === 'check') {
|
|
164
|
+
const task = readTask(repo, words[1]); const release = lock(repo, `task-${task.id}`);
|
|
165
|
+
try { return await runChecks(repo, task); } finally { release(); }
|
|
166
|
+
}
|
|
167
|
+
if (command === 'open') {
|
|
168
|
+
// --bot opens what one of the session's bots saved. --role still reads a name an earlier run saved under.
|
|
169
|
+
const task = readTask(repo, words[1]); const bot = flags.bot === undefined ? null : String(flags.bot).replace(/^@/, '');
|
|
170
|
+
const role = bot !== null ? botKey(bot) : flags.role ?? 'lead';
|
|
171
|
+
const session = Object.hasOwn(task.sessions, role) ? task.sessions[role] : null;
|
|
172
|
+
if (!session || !/^[a-f0-9-]{36}$/i.test(session.id)) refuse(bot !== null ? `@${bot} has no saved session in task ${task.id}.` : 'There is no saved session to open.');
|
|
173
|
+
if (task.state === 'running') refuse('Wait for the headless run to finish before opening its session.');
|
|
174
|
+
// The adapter names its resume command; a vendor with a private HOME names it in env.
|
|
175
|
+
const adapter = ADAPTERS[session.vendor];
|
|
176
|
+
const plan = adapter?.interactive?.({ session: session.id, cwd: task.worktree }) ?? { bin: session.vendor, args: session.vendor === 'codex' ? ['resume', session.id] : ['--resume', session.id] };
|
|
177
|
+
const prefix = Object.entries(plan.env ?? {}).map(([key, value]) => `${key}=${quote(value)} `).join('');
|
|
178
|
+
const info = { command: `cd ${quote(task.worktree)} && ${prefix}${plan.bin} ${plan.args.map(quote).join(' ')}`, message: "Opening uses the vendor's interactive CLI and your own approvals. ELU Code's headless policy does not apply." };
|
|
179
|
+
if (flags.exec) {
|
|
180
|
+
if (flags.json || !process.stdin.isTTY) refuse('Interactive opening needs a terminal. Use the printed command.');
|
|
181
|
+
process.stdout.write(`${info.message}\n`);
|
|
182
|
+
const { spawnSync } = await import('node:child_process');
|
|
183
|
+
const result = spawnSync(plan.bin, plan.args, { cwd: task.worktree, env: plan.env ? { ...process.env, ...plan.env } : process.env, stdio: 'inherit' });
|
|
184
|
+
return { ...info, exitCode: result.status };
|
|
185
|
+
}
|
|
186
|
+
return info;
|
|
187
|
+
}
|
|
188
|
+
if (command === 'uninstall') {
|
|
189
|
+
const plan = uninstallPlan(repo);
|
|
190
|
+
return await confirm(plan, flags) ? applyUninstall(repo, plan) : { ...plan, needsConfirmation: !flags['dry-run'] };
|
|
191
|
+
}
|
|
192
|
+
refuse(`Unknown command: ${words.join(' ')}. Use elu --help.`);
|
|
193
|
+
}
|
|
194
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
195
|
+
try {
|
|
196
|
+
const result = await main();
|
|
197
|
+
if (process.argv.includes('--json')) process.stdout.write(`${JSON.stringify({ success: true, ...result })}\n`);
|
|
198
|
+
else if (result.help) process.stdout.write(result.help);
|
|
199
|
+
else if (process.argv.includes('--version')) process.stdout.write(`${result.version}\n`);
|
|
200
|
+
else if (result.quiet) { /* Nothing to report. */ }
|
|
201
|
+
else process.stdout.write(formatResult(result,process.argv[2]));
|
|
202
|
+
if (result.results?.some((row) => row.status !== 'passed') || result.state === 'blocked'||result.sandboxHelper?.working===false||result.success===false) process.exitCode = 1;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const output = { success: false, message: error.message, ...error.details };
|
|
205
|
+
if (process.argv.includes('--json')) process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
206
|
+
else process.stderr.write(`${error.message}\n${error.details&&Object.keys(error.details).length?Object.entries(error.details).map(([key,value])=>`${key}: ${typeof value==='object'?JSON.stringify(value):value}`).join('\n')+'\n':''}`);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
package/bin/elu.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The first file every start runs. It is written for any Node to read, so an old Node gets
|
|
3
|
+
// one sentence here and not a syntax error. What only a current Node can read is in start.cjs.
|
|
4
|
+
'use strict';
|
|
5
|
+
var MINIMUM = 22;
|
|
6
|
+
if (!(parseInt(process.versions.node, 10) >= MINIMUM)) {
|
|
7
|
+
process.stderr.write('ELU Code needs Node ' + MINIMUM + ' or newer; this is Node ' + process.versions.node + '.\n');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
require('./start.cjs');
|
package/bin/start.cjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Loaded by elu.cjs once the Node is new enough to read this line.
|
|
3
|
+
import('../lib/install.mjs').then(function (install) { return install.start(); }).catch(function (error) {
|
|
4
|
+
process.stderr.write(String((error && error.stack) || error) + '\n');
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|